瀏覽代碼

Untested implementation of system state

Billy Barrow 6 月之前
父節點
當前提交
f1461826d8

+ 55 - 0
README.md

@@ -114,3 +114,58 @@ Last line consists of the signatures for the repository
 ## Complete package (package.usmc)
 
 Complete package is simply a tar.xz file containing a MANIFEST.usm file and all the requisite files that would be acquired if the acquire script was run. That is to say, a USMC file could be created by simply running `usm manifest acquire` followed by `tar -cJf ../package.usmc .`.
+
+
+
+
+## USM Directory /var/usm
+
+- /packages
+  - /packagename-0.1.1
+    - /origin-info
+    - /package.usmc
+    - /build-log (when built)
+    - /build.tar.xz (when built)
+    - /build (when building)
+- /installed
+  - /packagename-0.1.1 (symlink to /var/usm/packages/packagename-0.1.1)
+- /lists
+  - /repositoryname
+    - /2020-03-20T14:28:23.382748.usml
+    - /2020-03-20T14:34:42.382748.usml
+- /transactions
+  - 2020-03-21T14:28:23.382748
+  - 2020-03-23T14:23:43.382748
+
+### Origin info format
+```json
+{
+    "repository": "repositoryname",
+    "listfile": "2020-03-20T14:34:42.382748.usml",
+    "original_path": "packagename-0.1.1.usmc",
+    "signature_verified": true
+}
+```
+
+### Transaction format
+```json
+{
+    "complete": true, // False when in progress
+    "snapshot": "snapshot path", // For later when BTRFS snapshotting implemented, for rollbacks
+    "operations": [
+        {
+            "operation": "install", // or "remove"
+            "package": {
+                "name": "packagename",
+                "version": "0.1.1"
+            },
+            "origin" {
+                "repository": "repositoryname",
+                "listfile": "2020-03-20T14:34:42.382748.usml",
+                "original_path": "packagename-0.1.1.usmc",
+                "signature_verified": true
+            }
+        }
+    ]
+}
+```

+ 3 - 3
src/lib/Paths.vala

@@ -18,7 +18,7 @@ namespace Usm {
         public string shared_state { get; set; }
         public string sys_config { get; set; }
 
-        public string usm_repos_dir { get; set; }
+        public string usm_config_dir { get; set; }
         public string usm_state_dir { get; set; }
 
         public void set_envs() {
@@ -99,7 +99,7 @@ namespace Usm {
             shared_state = "com";
             sys_config = "etc";
 
-            usm_repos_dir = "/etc/usm/repos.d";
+            usm_config_dir = "/etc/usm";
             usm_state_dir = "/var/usm";
         }
 
@@ -121,7 +121,7 @@ namespace Usm {
             shared_state = Environment.get_variable("USM_SHAREDSTATEDIR") ?? defaults.shared_state;
             sys_config = Environment.get_variable("USM_SYSCONFIGDIR") ?? defaults.sys_config;
 
-            usm_repos_dir = Environment.get_variable("USM_REPOSDIR") ?? defaults.usm_repos_dir;
+            usm_config_dir = Environment.get_variable("USM_CONFIGDIR") ?? defaults.usm_config_dir;
             usm_state_dir = Environment.get_variable("USM_STATEDIR") ?? defaults.usm_state_dir;
         }
     }

+ 29 - 0
src/lib/Repository/GioRepositoryClient.vala

@@ -0,0 +1,29 @@
+namespace Usm {
+
+    public class GioRepositoryClient : RepositoryClient {
+
+        public GioRepositoryClient(Repository repo) {
+            repository_config = repo;
+        }
+
+        public override void download_repository_listing (string path, Usm.RepositoryClientProgressCallback? callback = null) throws Error {
+            var filename = "PACKAGES.usml";
+            var file = File.new_for_uri (get_for_path(filename).to_string());
+            file.copy(File.new_for_path (path), FileCopyFlags.OVERWRITE, null, (c, t) => call_to(callback, filename, c, t));
+        }
+
+        public override void download_package (string path, RepositoryListingEntry package, Usm.RepositoryClientProgressCallback? callback = null) throws Error {
+            var filename = Path.get_basename(package.path);
+            var file = File.new_for_uri (get_for_path(package.path).to_string());
+            file.copy(File.new_for_path (path), FileCopyFlags.OVERWRITE, null, (c, t) => call_to(callback, filename, c, t));
+        }
+
+        private Uri get_for_path(string path) throws Error {
+            var uri = Uri.parse (repository_config.url, UriFlags.NONE);
+            return Uri.build (UriFlags.NONE, uri.get_scheme (), uri.get_userinfo (), uri.get_host (), uri.get_port (), Path.build_filename (uri.get_path (), path), uri.get_query (), uri.get_fragment ());
+        }
+
+
+    }
+
+}

+ 22 - 1
src/lib/Repository/Repository.vala

@@ -17,7 +17,28 @@ namespace Usm {
                 cfg.map<string>("key", o => o.key.to_base64(), (o, v) => o.key = new BinaryData.from_base64(v));
                 cfg.set_constructor(() => new Repository());
             });
-        }       
+        }      
+        
+        
+        public Repository.from_file(string path) throws Error {
+            var element = new InvercargillJson.JsonElement.from_file(path);
+            Repository.get_mapper().map_into(this, element.as<Invercargill.Properties>());
+        }
+
+        public RepositoryClient get_client() throws Error {
+            var uri = Uri.parse(url, UriFlags.NONE);
+            var scheme = uri.get_scheme();
+
+            switch (scheme) {
+                case "http":
+                case "https":
+                case "file":
+                case "ftp":
+                    return new GioRepositoryClient(this);
+                default:
+                    assert_not_reached();
+            }
+        }
 
     }
 }

+ 31 - 3
src/lib/Repository/RepositoryClient.vala

@@ -1,3 +1,5 @@
+using Invercargill.Convert;
+
 namespace Usm {
 
     public delegate void RepositoryClientProgressCallback(string filename, int64 current, int64 total);
@@ -6,11 +8,37 @@ namespace Usm {
 
         public Repository repository_config { get; protected set; }
 
-        public abstract void download_repository_listing(string path, RepositoryClientProgressCallback? callback = null);
+        public abstract void download_repository_listing(string path, RepositoryClientProgressCallback? callback = null)  throws Error;
+
+        public abstract void download_package(string path, RepositoryListingEntry package, RepositoryClientProgressCallback? callback = null)  throws Error;
+
+        public virtual bool verify_package(string path, RepositoryListingEntry package, RepositoryClientProgressCallback? callback = null) throws Error {
+            var checksum = new Checksum(ChecksumType.SHA512);
+            var stream = File.new_for_path(path).read();
+
+            const int chunk_size = 1024 * 1024 * 128;
+            var buffer = new uint8[chunk_size];
+            while(true) {
+                var read = stream.read(buffer);
+                if(read == 0) {
+                    break;
+                }
+
+                checksum.update(buffer, read);
+            }
+
+            var checksum_bytes = new uint8[ChecksumType.SHA512.get_length()];
+            size_t size = checksum_bytes.length;
+            checksum.get_digest(checksum_bytes, ref size);
 
-        public abstract void download_package(string path, RepositoryListingEntry package, RepositoryClientProgressCallback? callback = null);
+            return package.sha512sum.equals(ate(checksum_bytes));
+        }
 
-        public abstract void verify_package(string path, RepositoryListingEntry package, RepositoryClientProgressCallback? callback = null);
+        protected void call_to(RepositoryClientProgressCallback? callback, string filename, int64 current, int64 total) {
+            if(callback != null) {
+                callback(filename, current, total);
+            }
+        }
 
     }
 

+ 91 - 0
src/lib/State/CachedPackage.vala

@@ -0,0 +1,91 @@
+using Invercargill;
+
+namespace Usm {
+
+    public class CachedPackage {
+
+        public string state_path { get; private set; }
+        public string package_path { get; private set; }
+        public string package_name { get; private set; }
+
+        public CachedPackage(string path) {
+            this.state_path = path;
+            this.package_path = Path.build_filename(path, "package.usmc");
+            this.package_name = Path.get_basename(path);
+        }
+
+        public Manifest get_manifest() throws Error {
+            return new Manifest.from_package(package_path);
+        }
+
+        public OriginInformation get_origin_information() throws Error {
+            return new OriginInformation.from_file(Path.build_filename(state_path, "origin-info"));
+        }
+
+        public bool has_build_directory() {
+            return File.new_for_path(build_directory_path()).query_exists();
+        }
+
+        public bool has_build_archive() {
+            return File.new_for_path(build_archive_path()).query_exists();
+        }
+
+        public string create_build_directory() throws Error {
+            var path = build_directory_path();
+            File.new_for_path(path).make_directory();
+            return path;
+        }
+
+        public string get_build_directory() throws Error {
+            var path = build_directory_path();
+            if(File.new_for_path(path).query_exists()) {
+                return path;
+            }
+
+            var archive_path = build_archive_path();
+            if(!File.new_for_path(path).query_exists()) {
+                throw new StateError.NO_BUILD_ARTIFACT(@"The package \"$package_name\" has no build artifact");
+            }
+
+            var proc = new Subprocess.newv(new string[] { "tar", "-xf", archive_path, "-C", path }, SubprocessFlags.INHERIT_FDS);
+            if(!proc.wait_check()) {
+                throw new StateError.EXTRACTION_FAILED(@"Could not extract \"$archive_path\": tar returned non-zero exit status $(proc.get_status())");
+            }
+            return path;
+        }
+
+        public string get_build_archive() throws Error {
+            var path = build_archive_path();
+            if(File.new_for_path(path).query_exists()) {
+                return path;
+            }
+
+            var dir_path = build_directory_path();
+            if(!File.new_for_path(path).query_exists()) {
+                throw new StateError.NO_BUILD_ARTIFACT(@"The package \"$package_name\" has no build artifact");
+            }
+
+            var proc = new Subprocess.newv(new string[] { "tar", "-cJf", path, dir_path }, SubprocessFlags.INHERIT_FDS);
+            if(!proc.wait_check()) {
+                throw new StateError.ARCHIVAL_FAILED(@"Could not archive \"$dir_path\": tar returned non-zero exit status $(proc.get_status())");
+            }
+            return path;
+        }
+
+        public void update_origin_information(OriginInformation info) throws Error {
+            var properties = OriginInformation.get_mapper().map_from(info);
+            var json = new InvercargillJson.JsonElement.from_properties(properties);
+            json.write_to_file(Path.build_filename(state_path, "origin-info"));
+        }
+
+        private string build_directory_path() {
+            return Path.build_filename(state_path, "build");
+        }
+
+        private string build_archive_path() {
+            return Path.build_filename(state_path, "build.tar.xz");
+        }
+
+    }
+
+}

+ 28 - 0
src/lib/State/OriginInformation.vala

@@ -0,0 +1,28 @@
+using Invercargill;
+
+namespace Usm {
+
+    public class OriginInformation {
+
+        public string repository { get; set; }
+        public string listfile { get; set; }
+        public string original_path { get; set; }
+        public bool signature_verified { get; set; }
+
+        public static PropertyMapper<OriginInformation> get_mapper() {
+            return PropertyMapper.build_for<OriginInformation>(cfg => {
+                cfg.map<string>("repository", o => o.repository, (o, v) => o.repository = v);
+                cfg.map<string>("listfile", o => o.listfile, (o, v) => o.listfile = v);
+                cfg.map<string>("original_path", o => o.original_path, (o, v) => o.original_path = v);
+                cfg.map<bool>("signature_verified", o => o.signature_verified, (o, v) => o.signature_verified = v);
+            });
+        }
+
+        public OriginInformation.from_file(string path) throws Error {
+            var element = new InvercargillJson.JsonElement.from_file(path);
+            OriginInformation.get_mapper().map_into(this, element.as<Invercargill.Properties>());
+        }
+
+    }
+
+}

+ 84 - 0
src/lib/State/State.vala

@@ -0,0 +1,84 @@
+using Invercargill;
+
+namespace Usm {
+
+    public class SystemState {
+
+        public string config_path { get; private set; }
+        public string state_path { get; private set; }
+
+        public SystemState(Paths paths) {
+            config_path = paths.usm_config_dir;
+            state_path = paths.usm_state_dir;
+        }
+
+        public Enumerable<CachedPackage> get_cached_packages() throws Error {
+            return directory(Path.build_filename(state_path, "packages"))
+                .select<CachedPackage>(d => new CachedPackage(Path.build_filename(state_path, "packages", d)));
+        }
+
+        public Enumerable<CachedPackage> get_installed_packages() throws Error {
+            return directory(Path.build_filename(state_path, "installed"))
+                .select<CachedPackage>(d => new CachedPackage(Path.build_filename(state_path, "installed", d)));
+        }
+
+        public Enumerable<Repository> get_repositories() throws Error {
+            return directory(Path.build_filename(config_path, "repos.d"))
+                .try_select<Repository>(d => new Repository.from_file(Path.build_filename(state_path, "repos.d", d)))
+                .unwrap_to_series();
+        }
+
+        public RepositoryListing? get_latest_list(Repository repository) throws Error {
+            var list_path = Path.build_filename(state_path, "lists", repository.name);
+            if(!File.new_for_path(list_path).query_exists()) {
+                return null;
+            }
+
+            var latest = directory(list_path)
+                .where(f => f.has_suffix(".usml"))
+                .contextualised_select<DateTime>(f => new DateTime.from_iso8601(f.replace(".usml", ""), null))
+                .sort((a, b) => a.result.compare(b.result))
+                .first_or_default();
+
+            var stream = new DataInputStream(File.new_build_filename(list_path, latest).read());
+            return new RepositoryListing.from_stream(stream);
+        }
+
+        public void refresh_list(Repository repository, RepositoryClientProgressCallback? callback = null) throws Error {
+            var client = repository.get_client();
+
+            var list_path = Path.build_filename(state_path, "lists", repository.name);
+            if(!File.new_for_path(list_path).query_exists()) {
+                File.new_for_path(list_path).make_directory();
+            }
+
+            var filename = @"$(new DateTime.now_utc().format_iso8601()).usml";
+            client.download_repository_listing(Path.build_filename(list_path, filename), callback);
+        }
+
+        public string generate_cache_path(Manifest manifest) {
+            var filename = @"$(manifest.name)-$(manifest.version)";
+            return Path.build_filename(state_path, "packages", filename);
+        }
+
+        public void mark_installed(CachedPackage package) throws Error {
+            var filename = Path.get_basename(package.state_path);
+            var symlink = File.new_build_filename(state_path, "installed", filename);
+            symlink.make_symbolic_link(package.state_path);
+        }
+
+        public void unmark_installed(CachedPackage package) throws Error {
+            var filename = Path.get_basename(package.state_path);
+            var symlink = File.new_build_filename(state_path, "installed", filename);
+            symlink.delete();
+        }
+
+    }
+
+    public errordomain StateError {
+        NO_BUILD_ARTIFACT,
+        EXTRACTION_FAILED,
+        ARCHIVAL_FAILED,
+    }
+
+}

+ 4 - 0
src/lib/meson.build

@@ -13,6 +13,10 @@ sources += files('Version.vala')
 sources += files('Repository/Repository.vala')
 sources += files('Repository/RepositoryListing.vala')
 sources += files('Repository/RepositoryClient.vala')
+sources += files('Repository/GioRepositoryClient.vala')
+sources += files('State/State.vala')
+sources += files('State/CachedPackage.vala')
+sources += files('State/OriginInformation.vala')
 
 
 dependencies = [