diff --git a/src/data/sources/steam/Steam.vala b/src/data/sources/steam/Steam.vala index ce3b43c7..51cfb433 100644 --- a/src/data/sources/steam/Steam.vala +++ b/src/data/sources/steam/Steam.vala @@ -19,6 +19,7 @@ along with GameHub. If not, see . using Gee; using GameHub.Data.DB; using GameHub.Utils; +using LevelDB; using ZLib.Utility; namespace GameHub.Data.Sources.Steam @@ -476,14 +477,25 @@ namespace GameHub.Data.Sources.Steam } public static void add_game_shortcut(Game game) + { + if(is_running()) + { + notify("[Sources.Steam.add_game_shortcut] No shortcut created because Steam is running, make sure Steam is closed properly."); + return; + } + set_shortcut(game); + set_shortcut_collection(game); + set_shortcut_assets(game); + } + + // add or update shortcut + private static void set_shortcut(Game game) { var config_dir = FSUtils.find_case_insensitive(get_userdata_dir(), "config"); if(config_dir == null || !config_dir.query_exists()) return; var shortcuts = FSUtils.find_case_insensitive(config_dir, "shortcuts.vdf") ?? FSUtils.file(config_dir.get_path(), "shortcuts.vdf"); - var vdf = new BinaryVDF(shortcuts); - var root_node = vdf.read() as BinaryVDF.ListNode; if(root_node.get("shortcuts") == null) @@ -495,56 +507,266 @@ namespace GameHub.Data.Sources.Steam root_node = root_node.get("shortcuts") as BinaryVDF.ListNode; } - var game_node = new BinaryVDF.ListNode.node(root_node.nodes.size.to_string()); + BinaryVDF.ListNode? game_node = null; + + foreach(BinaryVDF.Node node in root_node.nodes.values) + { + var tmp = node as BinaryVDF.ListNode; + var existing_node = tmp.get("LaunchOptions") as BinaryVDF.StringNode; + if(existing_node.value == "--run " + game.full_id) + { + game_node = tmp; + break; + } + } + + if(game_node == null) + { + game_node = new BinaryVDF.ListNode.node(root_node.nodes.size.to_string()); + } - game_node.add_node(new BinaryVDF.StringNode.node("AppName", game.name)); + game_node.add_node(new BinaryVDF.StringNode.node("appname", game.name)); game_node.add_node(new BinaryVDF.StringNode.node("exe", ProjectConfig.PROJECT_NAME)); game_node.add_node(new BinaryVDF.StringNode.node("LaunchOptions", "--run " + game.full_id)); game_node.add_node(new BinaryVDF.StringNode.node("ShortcutPath", ProjectConfig.DATADIR + "/applications/" + ProjectConfig.PROJECT_NAME + ".desktop")); game_node.add_node(new BinaryVDF.StringNode.node("StartDir", ".")); - game_node.add_node(new BinaryVDF.IntNode.node("IsHidden", 0)); - game_node.add_node(new BinaryVDF.IntNode.node("OpenVR", 0)); - game_node.add_node(new BinaryVDF.IntNode.node("AllowOverlay", 1)); - game_node.add_node(new BinaryVDF.IntNode.node("AllowDesktopConfig", 1)); - game_node.add_node(new BinaryVDF.IntNode.node("LastPlayTime", 1)); - if(game.image != null) + if(game.icon != null) + { + var cached = ImageCache.local_file(game.icon, @"games/$(game.source.id)/$(game.id)/icons/"); + game_node.add_node(new BinaryVDF.StringNode.node("icon", cached.get_path())); + } + else if(game.image != null) { var cached = ImageCache.local_file(game.image, @"games/$(game.source.id)/$(game.id)/images/"); game_node.add_node(new BinaryVDF.StringNode.node("icon", cached.get_path())); } + // add missing tags + var tags_node = game_node.get("tags") as BinaryVDF.ListNode; + if(tags_node == null) tags_node = new BinaryVDF.ListNode.node("tags"); + tags_node.add_node(new BinaryVDF.StringNode.node("0", "GameHub")); + + foreach(var tag in game.tags) + { + var tag_exists = false; + foreach(var value in tags_node.nodes.values) + { + var tmp = value as BinaryVDF.StringNode; + if(tag.name == tmp.value) + { + tag_exists = true; + break; + } + } + if(!tag_exists) + { + tags_node.add_node(new BinaryVDF.StringNode.node(tags_node.nodes.size.to_string(), tag.name)); + } + } + + game_node.add_node(tags_node); + root_node.add_node(game_node); + + root_node.show(); + + BinaryVDF.write(shortcuts, root_node); + } + + // add or update artwork + private static void set_shortcut_assets(Game game) + { + var custom_appid = generate_new_appid(ProjectConfig.PROJECT_NAME, game.name); + + if(game.image != null) + { + try + { + var cached = ImageCache.local_file(game.image, @"games/$(game.source.id)/$(game.id)/images/"); + var dest = FSUtils.file(get_userdata_dir().get_child("config").get_child("grid").get_path(), custom_appid + ".png"); + cached.copy(dest, FileCopyFlags.OVERWRITE); + } + catch (Error e) {} + } + if(game.image_vertical != null) { try { var cached = ImageCache.local_file(game.image_vertical, @"games/$(game.source.id)/$(game.id)/images/"); - // https://github.com/boppreh/steamgrid/blob/master/games.go#L120 - uint64 id = crc32(0, (ProjectConfig.PROJECT_NAME + game.name).data) | 0x80000000; - var dest = FSUtils.file(get_userdata_dir().get_child("config").get_child("grid").get_path(), id.to_string() + "p.png"); - cached.copy(dest, NONE); + var dest = FSUtils.file(get_userdata_dir().get_child("config").get_child("grid").get_path(), custom_appid + "p.png"); + cached.copy(dest, FileCopyFlags.OVERWRITE); } catch (Error e) {} } + } - var tags_node = new BinaryVDF.ListNode.node("tags"); - tags_node.add_node(new BinaryVDF.StringNode.node("0", "GameHub")); + // add or update collections + private static void set_shortcut_collection(Game game) + { + string? error; + var config_dir = FSUtils.find_case_insensitive(get_userdata_dir(), "config"); + if(config_dir == null || !config_dir.query_exists()) return; + + var collections_db = new SteamCollectionDatabase(instance.user_id, out error); + if(error != null) return; + + collections_db.read(out error); + if(error != null) return; + + var localconfig = Parser.parse_vdf_file(config_dir.get_path(), "localconfig.vdf"); + if(localconfig == null || localconfig.get_node_type() != Json.NodeType.OBJECT) return; + var user_collections = Parser.parse_json(localconfig.get_object().get_object_member("UserLocalConfigStore").get_object_member("WebStorage").get_string_member("user-collections")); + if(user_collections == null || user_collections.get_node_type() != Json.NodeType.OBJECT) return; + + int64? custom_appid_int; + int64.try_parse(generate_new_appid(ProjectConfig.PROJECT_NAME, game.name), out custom_appid_int); + if(custom_appid_int == null) return; + + // Remove from collections where the game doesn't have the tag anymore + user_collections.get_object().foreach_member((object, name, node) => + { + if(name.has_prefix("gh-") || name == "favorite" || name == "hidden") + { + foreach(var tag in game.tags) + { + string key; + if(tag.id == "builtin:favorites") + { + key = "favorite"; + } + else if(tag.id == "builtin:hidden") + { + key = "hidden"; + } + else + { + key = @"gh-$(tag.id)"; + } + + if(node.get_object().get_string_member("id") == key) return; + } + + if(node.get_object().has_member("added") && node.get_object().get_member("added").get_node_type() == Json.NodeType.ARRAY) + { + uint? index = null; + + node.get_object().get_array_member("added").foreach_element((a, i, n) => + { + if(n.get_int() == custom_appid_int) index = i; + }); + + if(index != null) + { + node.get_object().get_array_member("added").remove_element(index); + return; + } + } + } + }); + + // add new tags + var created_collection = false; foreach(var tag in game.tags) { - if(tag.removable) + string key; + if(tag.id == "builtin:favorites") + { + key = "favorite"; + } + else if(tag.id == "builtin:hidden") + { + key = "hidden"; + } + else + { + key = @"gh-$(tag.id)"; + } + + var collection = collections_db.get_collection(key); + + // create categorie if it doesn't exist already + if(collection == null && key.has_prefix("gh-")) + { + collection = new Json.Object(); + collection.set_string_member("id", key); + collection.set_string_member("name", tag.name); + collection.set_array_member("added", new Json.Array()); + collection.set_array_member("removed", new Json.Array()); + collections_db.set_collection(key, collection); + created_collection = true; + } + + if(!user_collections.get_object().has_member(key)) + { + var object = new Json.Object(); + object.set_string_member("id", key); + object.set_array_member("added", new Json.Array()); + object.set_array_member("removed", new Json.Array()); + user_collections.get_object().set_object_member(key, object); + } + + var already_present = false; + user_collections.get_object().get_object_member(key).get_array_member("added").foreach_element((array, index, node) => + { + if(node.get_int() == custom_appid_int) already_present = true; + }); + if(!already_present) { - tags_node.add_node(new BinaryVDF.StringNode.node((game.tags.index_of(tag) + 1).to_string(), tag.name)); + user_collections.get_object().get_object_member(key).get_array_member("added").add_int_element(custom_appid_int); } } - game_node.add_node(tags_node); + debug(@"[Sources.Steam.set_shortcut_collection]\n$(Json.to_string(user_collections, true))"); - root_node.add_node(game_node); + var generator = new Json.Generator(); + generator.set_root(user_collections); + localconfig.get_object().get_object_member("UserLocalConfigStore").get_object_member("WebStorage").set_string_member("user-collections", generator.to_data(null)); + FSUtils.write_string_to_file(FSUtils.file(config_dir.get_path(), "localconfig.vdf"), generate_vdf_from_json(localconfig.get_object())); + if(created_collection) collections_db.save(out error); + if(error != null) warning(@"[Sources.Steam.set_shortcut_collection] Error saving database: `%s`", error); + } - root_node.show(); + // https://github.com/node-steam/vdf/blob/master/src/index.ts#L78-L117 + private static string generate_vdf_from_json(Json.Object object, int level = 0) + { + var seperator = " "; + var result = ""; + var indent = ""; - BinaryVDF.write(shortcuts, root_node); + for(var i = 0; i < level; i++) + { + indent += seperator; + } + + object.foreach_member((object, name, node) => + { + if(node.get_node_type() == Json.NodeType.OBJECT && node.get_object() != null) + { + result += indent + "\"" + name + "\"\n" + indent + "{\n" + generate_vdf_from_json(node.get_object(), level + 1) + indent + "}\n"; + } + else + { + var generator = new Json.Generator(); + generator.set_root(node); + result += indent + "\"" + name + "\"" + seperator + seperator + generator.to_data(null) + "\n"; + } + }); + + return result; + } + + public static bool is_running() + { + if(Utils.run({"pidof", "steam"}).run_sync(true).exit_code == 0) return true; + return false; + } + + public static string generate_new_appid(string exe, string name) + { + // https://github.com/boppreh/steamgrid/blob/master/games.go#L120 + return (crc32(0, (exe + name).data) | 0x80000000).to_string(); } public static bool IsAnyAppRunning = false; diff --git a/src/data/sources/steam/SteamCollectionDatabase.vala b/src/data/sources/steam/SteamCollectionDatabase.vala new file mode 100644 index 00000000..6b81877f --- /dev/null +++ b/src/data/sources/steam/SteamCollectionDatabase.vala @@ -0,0 +1,340 @@ +using Gee; +using GameHub.Utils; +using LevelDB; + +namespace GameHub.Data.Sources.Steam +{ + private class SteamCollectionDatabase + { + private string db_path = FSUtils.Paths.Steam.Home + "/" + FSUtils.Paths.Steam.LevelDB; + private string steamid3; + private LevelDB.Database db; + private LevelDB.Options db_options = new LevelDB.Options(); + private LevelDB.ReadOptions db_read_options = new LevelDB.ReadOptions(); + private LevelDB.WriteOptions db_write_options = new LevelDB.WriteOptions(); + private ByteArray key_prefix = new ByteArray(); + private ByteArray namespaces_prefix = new ByteArray(); + private HashMap namespace_collections = new HashMap(); + private LinkedList namespaces = new LinkedList(); + + public SteamCollectionDatabase(string communityid, out string? error) + { + steamid3 = Steam.communityid_to_steamid3(uint64.parse(communityid)).to_string(); + + // "_https://steamloopback.host\x0\x1U$(steamid3)-cloud-storage-namespace" + key_prefix.append("_https://steamloopback.host".data); + key_prefix.append({ 0, 1 }); + key_prefix.append(@"U$(steamid3)-cloud-storage-namespace".data); + + // "_https://steamloopback.host\x0\x1U$(steamid3)-cloud-storage-namespaces" + namespaces_prefix = new ByteArray.take(key_prefix.data); + namespaces_prefix.append("s".data); + + db_options.set_create_if_missing(false); + + db = new LevelDB.Database(db_options, db_path, out error); + if(error != null) return; + } + + // Initially read the database and convert values we care about into json + public void read(out string? error) + { + // 0x01[[1,"413"], ...] + var namespaces_raw_json = db.get(db_read_options, namespaces_prefix.data, out error); + if(error != null) return; + + var namespaces_json = Parser.parse_json(((string) prepare_bytes(namespaces_raw_json).data)); + if(namespaces_json == null || namespaces_json.get_node_type() != Json.NodeType.ARRAY) return; + + namespaces_json.get_array().foreach_element((array, index, node) => + { + // [1,"413"] + if(node == null || node.get_node_type() != Json.NodeType.ARRAY) return; + if(node.get_array().get_length() < 1) return; + + // "_https://steamloopback.host\x0\x1U$(steamid3)-cloud-storage-namespace-1" + var namespace_key = new ByteArray.take(key_prefix.data); + namespace_key.append(@"-$(node.get_array().get_int_element(0))".data); + if(!namespaces.add(namespace_key)) return; + }); + + bool abort = false; + namespaces.foreach((namespace_key) => + { + string? e; + + var namespace_value = db.get(db_read_options, namespace_key.data, out e); + if(namespace_value == null || e != null) + { + warning(@"[Sources.Steam.SteamCollectionsDatabase.Read] Error reading namespace: `%s`: `%s`", (string) prepare_bytes(namespace_key.data).data, e); + return false; + } + + // debug_bytes(namespace_value); + var namespace_json = unserialize_collections((string) prepare_bytes(namespace_value).data); + if(namespace_json == null) + { + abort = true; + return false; + } + + namespace_collections.set(namespace_key, namespace_json); + return true; + }); + + if(abort) + { + error = "Error parsing json"; + return; + } + } + + // Example: + // [ + // "user-collections.gh-gamehub", + // { + // "key": "user-collections.gh-gamehub", + // "timestamp": 1587322550, + // "value": { + // "id": "gh-gamehub", + // "name": "gamehub", + // "added": [ + // ], + // "removed":[ + // ] + // }, + // "conflictResolutionMethod": "custom", + // "strMethodId": "union-collections" + // } + // ] + private Json.Array? @get(string id) + { + foreach(var collection_set in namespace_collections.values) + { + var collections = collection_set.get_array().get_elements(); + + foreach(var collection in collections) + { + if(collection.get_array().get_string_element(0) == id) + { + return collection.get_array(); + } + } + } + return null; + } + + // This returns a json object associated to an id which is included in the "value" member. + // Example: + // { + // "id" : "gh-gamehub", + // "name" : "gamehub", + // "added" : [ + // ], + // "removed" : [ + // ] + // } + public Json.Object? get_collection(string id) + { + var collection = @get(@"user-collections.$(id)"); + + if(collection != null && !collection.get_object_element(1).has_member("is_deleted") && collection.get_object_element(1).has_member("value") && collection.get_object_element(1).get_member("value").get_node_type() == Json.NodeType.OBJECT) + { + return collection.get_object_element(1).get_object_member("value"); + } + return null; + } + + public GLib.List get_collections_with_game(int64 appid) + { + var filtered_collections = new GLib.List(); + + foreach(var collection_set in namespace_collections.values) + { + var collections = collection_set.get_array().get_elements(); + foreach(var collection in collections) + { + if(collection.get_array().get_element(1).get_node_type() == Json.NodeType.OBJECT) + { + if(collection.get_array().get_object_element(1).has_member("value") && collection.get_array().get_object_element(1).get_member("value").get_node_type() == Json.NodeType.OBJECT) + { + if(collection.get_array().get_object_element(1).get_object_member("value").has_member("added") && collection.get_array().get_object_element(1).get_object_member("value").get_member("added").get_node_type() == Json.NodeType.ARRAY) + { + collection.get_array().get_object_element(1).get_object_member("value").get_array_member("added").foreach_element((array, index, node) => + { + if(node.get_int() == appid) filtered_collections.append(collection.get_array().get_object_element(1).get_object_member("value")); + }); + } + } + } + } + } + return filtered_collections.copy(); + } + + // add or update a collection + public void set_collection(string id, Json.Object value) + { + Json.Object? object = null; + var array = @get(@"user-collections.$(id)"); + if(array == null) + { + array = new Json.Array(); + array.add_string_element(@"user-collections.$(id)"); + } + + if(array.get_length() > 1) + { + object = array.get_object_element(1); + } + + if(object == null || object.has_member("is_deleted")) + { + object = new Json.Object(); + object.set_string_member("key", @"user-collections.$(id)"); + object.set_string_member("conflictResolutionMethod", "custom"); + object.set_string_member("strMethodId", "union-collections"); + array.add_object_element(object); + } + + object.set_int_member("timestamp", new DateTime.now_utc().to_unix()); + object.set_object_member("value", value); + + // Update collection if already present + foreach(var collection_set in namespace_collections.values) + { + var collections = collection_set.get_array().get_elements(); + foreach(var collection in collections) + { + if(collection.get_array().get_string_element(0) == @"user-collections.$(id)") + { + collection.get_array().remove_element(1); + collection.get_array().add_object_element(object); + return; + } + } + } + + // collection is new, add it to the last namespace + var collections = namespace_collections.get(namespaces.last()); + collections.get_array().add_array_element(array); + namespace_collections.set(namespaces.last(), collections); + } + + // First byte is 0x01 which causes trouble converting into a string, strip it + // Last byte isn't always zero so make sure we can get a string by terminating with zero by ourself + private ByteArray prepare_bytes(uint8[] raw_bytes) + { + var bytes = new ByteArray.take(raw_bytes[1:raw_bytes.length]); + bytes.append({ 0 }); + return bytes; + } + + private Json.Node? unserialize_collections(string raw_iso) + { + string? raw_json; + try + { + // string has 'e4' for 'ä' so the character set can be Cp1252, ISO 8859-1 or ISO 8859-15 + // according to some random website ISO 8859-1 is often used for html ¯\_(ツ)_/¯ + raw_json = convert(raw_iso, -1, "UTF-8", "ISO 8859-1"); + } catch (Error e) {return null;} + + var root = Parser.parse_json(raw_json); + if(root == null || root.get_node_type() != Json.NodeType.ARRAY) return null; + + root.get_array().foreach_element((array, index, node) => + { + if(node == null || node.get_node_type() != Json.NodeType.ARRAY) return; + + var object = node.get_array().get_object_element(1); + if(object == null) return; + + if(object.has_member("value") && object.get_member("value").get_value_type() == Type.STRING) + { + if(Parser.parse_json((string) object.get_string_member("value")).get_node_type() == Json.NodeType.OBJECT) + { + object.set_member("value", Parser.parse_json((string) object.get_string_member("value"))); + } + } + }); + + return root; + } + + private string? serialize_collections(Json.Node root) + { + if(root.get_node_type() != Json.NodeType.ARRAY) return null; + string? raw_json; + var generator = new Json.Generator(); + + root.get_array().foreach_element((array, index, node) => + { + if(node.get_node_type() != Json.NodeType.ARRAY) return; + if(node.get_array().get_object_element(1).has_member("value") && node.get_array().get_object_element(1).get_member("value").get_node_type() == Json.NodeType.OBJECT) + { + var object = node.get_array().get_object_element(1).get_object_member("value"); + if(object == null) return; + var new_object = new Json.Node(Json.NodeType.OBJECT); + new_object.init_object(object); + generator.set_root(new_object); + node.get_array().get_object_element(1).remove_member("value"); + node.get_array().get_object_element(1).set_string_member("value", generator.to_data(null)); + } + }); + generator.set_root(root); + raw_json = generator.to_data(null); + + try + { + var raw_iso = convert(raw_json, -1, "ISO 8859-1", "UTF-8"); + + // We've stripped the first byte, add it back + return @"\x1$(raw_iso)"; + } catch (Error e) {return null;} + } + + public void save(out string? error) + { + var write_batch = new LevelDB.WriteBatch(); + + namespaces.foreach((k) => + { + var raw_string = serialize_collections(namespace_collections.get(k)).data; + if(raw_string == null) return false; + + debug(@"[Sources.Steam.SteamCollectionsDatabase.Save]\n$(serialize_collections(namespace_collections.get(k)), false)"); + write_batch.put(k.data, raw_string); + return true; + }); + + try + { + if(!FSUtils.copy(FSUtils.file(db_path), FSUtils.file(db_path + "~"), FileCopyFlags.OVERWRITE)) + { + error = "Failed creating backup"; + return; + } + } + catch (Error e) + { + error = e.message; + return; + } + + write_batch.write(db, db_write_options, out error); + if(error != null) return; + } + + private void debug_bytes(int8[] bytes) + { + // View raw bytes for debugging purposes + string tmp = ""; + for(int i = 0; i < bytes.length; i++) + { + tmp = tmp + " %2x".printf(bytes[i]); + } + debug(tmp); + } + } +} diff --git a/src/meson.build b/src/meson.build index 2acdeac9..9f194025 100644 --- a/src/meson.build +++ b/src/meson.build @@ -22,6 +22,10 @@ project_config = vcs_tag( fallback: '' ) +# vala compiler flags +vapi_dir = meson.current_source_dir() / 'vapi' +add_project_arguments('--vapidir=' + vapi_dir, language: 'vala') + deps = [ dependency('gdk-3.0'), dependency('json-glib-1.0'), @@ -30,8 +34,10 @@ deps = [ dependency('libxml-2.0'), dependency('gio-unix-2.0'), dependency('zlib'), + dependency('leveldb'), meson.get_compiler('vala').find_library('posix'), - meson.get_compiler('vala').find_library('linux') + meson.get_compiler('vala').find_library('linux'), + meson.get_compiler('vala').find_library('leveldb', dirs: vapi_dir) ] sources = [ @@ -44,6 +50,7 @@ sources = [ 'data/sources/steam/Steam.vala', 'data/sources/steam/SteamGame.vala', + 'data/sources/steam/SteamCollectionDatabase.vala', 'data/sources/gog/GOG.vala', 'data/sources/gog/GOGGame.vala', diff --git a/src/utils/FSUtils.vala b/src/utils/FSUtils.vala index c90b72da..0d9d9a23 100644 --- a/src/utils/FSUtils.vala +++ b/src/utils/FSUtils.vala @@ -107,6 +107,8 @@ namespace GameHub.Utils public const string AppInfoVDF = "steam/appcache/appinfo.vdf"; public const string PackageInfoVDF = "steam/appcache/packageinfo.vdf"; + + public const string LevelDB = "steam/config/htmlcache/Local Storage/leveldb"; } public class GOG @@ -525,5 +527,53 @@ namespace GameHub.Utils } #endif } + + public static void write_string_to_file(File? file, string? data) + { + if(file == null || data == null) return; + + try + { + var stream = new DataOutputStream(file.replace(null, true, FileCreateFlags.NONE)); + stream.set_byte_order(DataStreamByteOrder.LITTLE_ENDIAN); + stream.put_string(data); + stream.flush(); + stream.close(); + } + catch(Error e) + { + warning("[FSUtils.write_string_to_file] Error writing `%s`: %s", file.get_path(), e.message); + } + } + + // https://stackoverflow.com/a/16454105 + public static bool copy(File src, File dest, FileCopyFlags flags=FileCopyFlags.NONE, Cancellable? cancellable=null) throws GLib.Error + { + FileType src_type = src.query_file_type(FileQueryInfoFlags.NONE, cancellable); + if(src_type == FileType.DIRECTORY) + { + DirUtils.create_with_parents(dest.get_path(), 777); + src.copy_attributes(dest, flags, cancellable); + + string src_path = src.get_path(); + string dest_path = dest.get_path(); + FileEnumerator enumerator = src.enumerate_children(FileAttribute.STANDARD_NAME, FileQueryInfoFlags.NONE, cancellable); + for(FileInfo? info = enumerator.next_file(cancellable); info != null; info = enumerator.next_file(cancellable)) + { + // copy recursive + copy( + File.new_for_path(Path.build_filename (src_path, info.get_name ())), + File.new_for_path(Path.build_filename (dest_path, info.get_name ())), + flags, + cancellable); + } + } + else if(src_type == FileType.REGULAR) + { + src.copy(dest, flags, cancellable); + } + + return true; + } } } diff --git a/src/utils/Parser.vala b/src/utils/Parser.vala index d40d692a..372be658 100644 --- a/src/utils/Parser.vala +++ b/src/utils/Parser.vala @@ -293,8 +293,8 @@ namespace GameHub.Utils return buf.content(); } - public delegate void JsonBulderDelegate(Json.Builder builder); - public static Json.Node json(JsonBulderDelegate? d=null) + public delegate void JsonBuilderDelegate(Json.Builder builder); + public static Json.Node json(JsonBuilderDelegate? d=null) { var builder = new Json.Builder(); builder.begin_object(); diff --git a/src/vapi/leveldb.vapi b/src/vapi/leveldb.vapi new file mode 100644 index 00000000..3bc5be7e --- /dev/null +++ b/src/vapi/leveldb.vapi @@ -0,0 +1,163 @@ +/* LevelDB Vala Bindings + * Copyright 2012 Evan Nemerson + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +[CCode (cheader_filename = "leveldb/c.h")] +namespace LevelDB { + [Compact, CCode (cname = "leveldb_t", lower_case_cprefix = "leveldb_", free_function = "leveldb_close")] + public class Database { + [CCode (cname = "leveldb_open")] + public Database (LevelDB.Options options, string name, out string? err); + + public LevelDB.Iterator create_iterator (LevelDB.ReadOptions options); + public LevelDB.Snapshot create_snapshot (); + public void delete (LevelDB.WriteOptions options, [CCode (array_length_type = "size_t")] uint8[] key, out string? err); + [CCode (cname = "leveldb_get", array_length_pos = 2.9, array_length_type = "size_t")] + public uint8[]? get (LevelDB.ReadOptions options, [CCode (array_length_type = "size_t")] uint8[] key, out string? err); + [CCode (cname = "leveldb_property_value")] + public string get_property_value (string propname); + public void put (LevelDB.WriteOptions options, [CCode (array_length_type = "size_t")] uint8[] key, [CCode (array_length_type = "size_t")] uint8[] val, out string? err); + public void write (LevelDB.WriteOptions options, LevelDB.WriteBatch batch, out string? err); + + [CCode (cname = "leveldb_destroy_db")] + public static void destroy (LevelDB.Options options, string name, out string? err); + [CCode (cname = "leveldb_repair_db")] + public static void repair (LevelDB.Options options, string name, out string? err); + } + + [Compact, CCode (cname = "leveldb_cache_t", free_function = "leveldb_cache_destroy")] + public class Cache { + private Cache (); + [CCode (cname = "leveldb_cache_create_lru")] + public Cache.lru (size_t capacity); + } + + [CCode (instance_pos = 0.1)] + public delegate int CompareFunc ([CCode (array_length_type = "size_t")] uint8[] a, [CCode (array_length_type = "size_t")] uint8[] b); + + [CCode (has_target = false)] + public delegate string NameFunc (); + + [Compact, CCode (cname = "leveldb_comparator_t", free_function = "leveldb_comparator_destroy")] + public class Comparator { + [CCode (cname = "leveldb_comparator_create")] + public Comparator ([CCode (delegate_target_pos = 0.1, type = "int (*)(void*,const char*,size_t,const char*,size_t)")] owned LevelDB.CompareFunc compare, [CCode (type = "const char* (*)(void*)")] LevelDB.NameFunc name); + } + + [CCode (cname = "int", has_type_id = false)] + public enum Compression { + [CCode (cname = "leveldb_no_compression")] + NONE, + [CCode (cname = "leveldb_snappy_compression")] + SNAPPY + } + + [Compact, CCode (cname = "leveldb_env_t", free_function = "leveldb_env_destroy")] + public class Environment { + private Environment (); + [CCode (cname = "leveldb_create_default_env")] + public Environment.default (); + } + + [Compact, CCode (cname = "leveldb_iterator_t", lower_case_cprefix = "leveldb_iter_", free_function = "leveldb_iter_destroy")] + public class Iterator { + public bool valid (); + public void seek_to_first (); + public void seek_to_last (); + public void seek ([CCode (type = "const char*", array_length_type = "size_t")] uint8[] k); + public void next (); + public void prev (); + [CCode (array_length_type = "size_t")] + public uint8[] key (); + [CCode (array_length_type = "size_t")] + public uint8[] value (); + public void get_error (out string? err); + } + + [Compact, CCode (cname = "leveldb_logger_t")] + public class Logger { + private Logger (); + } + + [Compact, CCode (cname = "leveldb_options_t", lower_case_cprefix = "leveldb_options_", free_function = "leveldb_options_destroy")] + public class Options { + [CCode (cname = "leveldb_options_create")] + public Options (); + + public void set_comparator (LevelDB.Comparator comparator); + public void set_create_if_missing (bool compare_if_missing); + public void set_error_if_exists (bool error_if_exists); + public void set_paranoid_checks (bool paranoid_checks); + public void set_env (LevelDB.Environment env); + public void set_info_log (LevelDB.Logger info_log); + public void set_write_buffer (size_t write_buffer); + public void set_max_open_files (int max_open_files); + public void set_cache (LevelDB.Cache cache); + public void set_block_size (size_t block_size); + public void set_block_restart_interval (int block_restart_interval); + public void set_compression (LevelDB.Compression compression); + } + + [Compact, CCode (cname = "leveldb_readoptions_t", lower_case_cprefix = "leveldb_readoptions_", free_function = "leveldb_readoptions_destroy")] + public class ReadOptions { + [CCode (cname = "leveldb_readoptions_create")] + public ReadOptions (); + + public void set_fill_cache (bool fill_cache); + public void set_snapshot (LevelDB.Snapshot snapshot); + public void set_verify_checksums (bool verify_checksums); + } + + [Compact, CCode (cname = "leveldb_snapshot_t", free_function = "leveldb_release_snapshot")] + public class Snapshot { + [CCode (cname = "leveldb_create_snapshot")] + public Snapshot (LevelDB.Database db); + } + + [Compact, CCode (cname = "leveldb_writeoptions_t", lower_case_cprefix = "leveldb_writeoptions_", free_function = "leveldb_writeoptions_destroy")] + public class WriteOptions { + [CCode (cname = "leveldb_writeoptions_create")] + public WriteOptions (); + + public void set_sync (bool sync); + } + + [Compact, CCode (cname = "leveldb_writebatch_t", free_function = "leveldb_writebatch_destroy", lower_case_cprefix = "leveldb_writebatch_")] + public class WriteBatch { + [CCode (cname = "leveldb_writebatch_create")] + public WriteBatch (); + + [CCode (has_target = false, simple_generics = true)] + public delegate void PutFunc (T state, [CCode (array_length_type = "size_t")] uint8[] key, [CCode (array_length_type = "size_t")] uint8[] val); + [CCode (has_target = false, simple_generics = true)] + public delegate void DeleteFunc (T state, [CCode (array_length_type = "size_t")] uint8[] key); + + public void clear (); + public void delete ([CCode (array_length_type = "size_t")] uint8[] key); + [CCode (simple_generics = true)] + public void iterate (T state, LevelDB.WriteBatch.PutFunc put, LevelDB.WriteBatch.DeleteFunc delete); + public void put ([CCode (array_length_type = "size_t")] uint8[] key, [CCode (array_length_type = "size_t")] uint8[] val); + [CCode (cname = "leveldb_write", instance_pos = 2.5)] + public void write (LevelDB.Database db, LevelDB.WriteOptions options, out string? err); + } +}