|
@@ -7,6 +7,8 @@ namespace Binman {
|
|
|
public ApplicationType application_type { get; private set; }
|
|
|
|
|
|
private Enumerable<ManifestFile> manifests;
|
|
|
+ private Dictionary<ManifestFile, ManifestHeader> headers;
|
|
|
+ private Dictionary<ManifestFile, BlobStore> blob_stores;
|
|
|
private Dictionary<string, ManifestFile> path_ownership;
|
|
|
|
|
|
public Applicator(ApplicationType application_type, Enumerable<CompositionComponent>? composition = null) throws Error {
|
|
@@ -22,28 +24,146 @@ namespace Binman {
|
|
|
.try_select<ManifestFile>(c => new ManifestFile.from_name(c.name, application_type, c.key))
|
|
|
.to_series();
|
|
|
|
|
|
+ headers = new Dictionary<ManifestFile, ManifestHeader>();
|
|
|
+ blob_stores = new Dictionary<ManifestFile, BlobStore>();
|
|
|
path_ownership = new Dictionary<string, ManifestFile>();
|
|
|
foreach (var manifest in manifests) {
|
|
|
+ var header = manifest.read_header();
|
|
|
+ headers[manifest] = header;
|
|
|
+ blob_stores[manifest] = new BlobStore(manifest, header);
|
|
|
foreach (var entry in manifest.get_entries()) {
|
|
|
path_ownership[entry.unwrap().path] = manifest;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- public void apply() {
|
|
|
-
|
|
|
+ public void apply() throws Error {
|
|
|
+ // Figure out what files we need
|
|
|
+ var details = get_file_details().to_series();
|
|
|
+ var need_applied = details
|
|
|
+ .where(f => f.installation_status != FileStatus.GOOD)
|
|
|
+ .cache();
|
|
|
+
|
|
|
+ var need_downloaded = need_applied
|
|
|
+ .where(f => f.file_description.binary != null)
|
|
|
+ .where(f => !blob_stores.get_or_default(f.originating_manifest).has_descriptor(f.file_description.binary, true))
|
|
|
+ .cache();
|
|
|
+
|
|
|
+ // Download and verify the files we need
|
|
|
+ foreach (var file in need_downloaded) {
|
|
|
+ var blob_file = blob_stores[file.originating_manifest].get_file_for_descriptor(file.file_description.binary);
|
|
|
+ download_file(file.file_description.binary, file.originating_manifest, blob_file);
|
|
|
+ posix_to_error(Posix.chmod(blob_file.get_path(), 0600));
|
|
|
+ if(!verify_file(file.file_description.binary, blob_file)) {
|
|
|
+ throw new IOError.INVALID_DATA(@"Checksum mismatch on file \"$(file.file_description.path)\" from manifest \"$(headers[file.originating_manifest].name)\"");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Apply each file
|
|
|
+ foreach (var file in need_applied) {
|
|
|
+ apply_file(file);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void apply_file(FileDetails file) throws Error {
|
|
|
+ // Case where file is already applied properly
|
|
|
+ if(file.installation_status == FileStatus.GOOD){
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Delete the system file if the type or content is bad
|
|
|
+ if(file.installation_status.any_set(FileStatus.BAD_TYPE | FileStatus.BAD_CONTENT)) {
|
|
|
+ posix_to_error(Posix.unlink(file.file_description.path));
|
|
|
+
|
|
|
+ // If mode is zeroed out, it is a blanking entry, don't continue
|
|
|
+ if(file.file_description.mode == 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Cases where we need to create the file
|
|
|
+ var is_newly_create_file = file.installation_status.any_set(FileStatus.MISSING | FileStatus.BAD_TYPE | FileStatus.BAD_CONTENT);
|
|
|
+ if(is_newly_create_file) {
|
|
|
+ if(Posix.S_ISDIR(file.file_description.mode)) {
|
|
|
+ posix_to_error(Posix.mkdir(file.file_description.path, file.file_description.mode));
|
|
|
+ }
|
|
|
+ else if(Posix.S_ISFIFO(file.file_description.mode)) {
|
|
|
+ posix_to_error(Posix.mkfifo(file.file_description.path, file.file_description.mode));
|
|
|
+ }
|
|
|
+ else if(Posix.S_ISLNK(file.file_description.mode)) {
|
|
|
+ posix_to_error(Posix.symlink(file.file_description.target, file.file_description.path));
|
|
|
+ }
|
|
|
+ else if(Posix.S_ISREG(file.file_description.mode) && file.file_description.binary == null) {
|
|
|
+ posix_to_error(Posix.creat(file.file_description.path, file.file_description.mode));
|
|
|
+ }
|
|
|
+ else if(Posix.S_ISREG(file.file_description.mode) && file.file_description.binary != null) {
|
|
|
+ reflink_copy(blob_stores[file.originating_manifest].get_path_for_discriptor(file.file_description.binary), file.file_description.path);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ throw new IOError.NOT_SUPPORTED(@"No implemented way to create entry with specifed mode");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Cases where we need to set the owner or group
|
|
|
+ if(is_newly_create_file || file.installation_status.any_set(FileStatus.BAD_OWNER | FileStatus.BAD_GROUP)) {
|
|
|
+ if(Posix.S_ISLNK(file.file_description.mode)) {
|
|
|
+ posix_to_error(Posix.lchown(file.file_description.path, file.file_description.user, file.file_description.group));
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ posix_to_error(Posix.chown(file.file_description.path, file.file_description.user, file.file_description.group));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Cases where we need to set the permissions
|
|
|
+ if(is_newly_create_file || file.installation_status.any_set(FileStatus.BAD_PERMISSIONS)) {
|
|
|
+ if(Posix.S_ISLNK(file.file_description.mode) && (file.file_description.mode & 0777) != 0777) {
|
|
|
+ throw new IOError.NOT_SUPPORTED("Symlink with permissions other than 0777 is not supported");
|
|
|
+ }
|
|
|
+ posix_to_error(Posix.chmod(file.file_description.path, file.file_description.mode));
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- public Dictionary<string, FileStatus> check_files() throws Error {
|
|
|
+ public void download_file(ManifestFileDescriptor descriptor, ManifestFile manifest, File destination, FileProgressCallback? progress_callback = null) throws Error {
|
|
|
+ var header = headers.get_or_default(manifest) ?? manifest.read_header();
|
|
|
+ var downloaded = false;
|
|
|
+ foreach (var remote in header.remotes) {
|
|
|
+ try {
|
|
|
+ var remote_file = File.new_for_uri(Uri.resolve_relative(remote, descriptor.remote_path, UriFlags.NONE));
|
|
|
+ remote_file.copy(destination, FileCopyFlags.OVERWRITE | FileCopyFlags.TARGET_DEFAULT_PERMS | FileCopyFlags.TARGET_DEFAULT_MODIFIED_TIME, null, progress_callback);
|
|
|
+ downloaded = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ catch(Error e) {
|
|
|
+ warning(@"Download \"$(descriptor.remote_path)\" from remote \"$remote\" failed: $(e.message)");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if(!downloaded) {
|
|
|
+ throw new IOError.FAILED("Could not download file, all remotes were tried.");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public bool verify_file(ManifestFileDescriptor descriptor, File file, FileProgressCallback? progress_callback = null) throws Error {
|
|
|
+ return !calculate_file_checksum(file.get_path(), progress_callback).equals(descriptor.checksum);
|
|
|
+ }
|
|
|
+
|
|
|
+ public Attempts<FileDetails> get_file_details() throws Error {
|
|
|
return manifests
|
|
|
- .try_select_nested<ManifestEntry>(m => m.get_entries().where(e => path_ownership.get_or_default(e.path) == m))
|
|
|
- .select_to_dictionary<string, FileStatus>(e => e.path, e => check_installed_file(e));
|
|
|
+ .try_select_nested<FileDetails>(m => m.get_entries()
|
|
|
+ .where(e => path_ownership.get_or_default(e.path) == m)
|
|
|
+ .select<FileDetails>(e => new FileDetails() {
|
|
|
+ originating_manifest = m,
|
|
|
+ file_description = e,
|
|
|
+ installation_status = check_installed_file(e)
|
|
|
+ })
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
public FileStatus check_installed_file(ManifestEntry entry) {
|
|
|
Posix.Stat stat_info;
|
|
|
if(Posix.stat(entry.path, out stat_info) != 0) {
|
|
|
- return FileStatus.MISSING;
|
|
|
+ // If mode is zero, file is supposed to be deleted
|
|
|
+ return entry.mode == 0 ? FileStatus.GOOD : FileStatus.MISSING;
|
|
|
}
|
|
|
|
|
|
var status = FileStatus.GOOD;
|
|
@@ -54,9 +174,14 @@ namespace Binman {
|
|
|
status |= FileStatus.BAD_OWNER;
|
|
|
}
|
|
|
|
|
|
- if(stat_info.st_mode != entry.mode){
|
|
|
- status |= FileStatus.BAD_MODE;
|
|
|
+ if((stat_info.st_mode | 0777) != (entry.mode | 0777)){
|
|
|
+ status |= FileStatus.BAD_TYPE;
|
|
|
}
|
|
|
+
|
|
|
+ if((stat_info.st_mode | ~0777) != (entry.mode | ~0777)){
|
|
|
+ status |= FileStatus.BAD_PERMISSIONS;
|
|
|
+ }
|
|
|
+
|
|
|
else if(Posix.S_ISLNK(entry.mode)) {
|
|
|
var buffer = new char[uint16.MAX];
|
|
|
buffer.length = (int)Posix.readlink(entry.path, buffer);
|
|
@@ -83,18 +208,20 @@ namespace Binman {
|
|
|
return status;
|
|
|
}
|
|
|
|
|
|
- public BinaryData calculate_file_checksum(string path) throws Error {
|
|
|
+ public static BinaryData calculate_file_checksum(string path, FileProgressCallback? progress_callback = null) throws Error {
|
|
|
var stream = File.new_for_path(path).read();
|
|
|
var checksum = new Checksum(ChecksumType.SHA512);
|
|
|
|
|
|
const int increment = 1024 * 1024 * 128;
|
|
|
uint64 read = 0;
|
|
|
- var to_read = stream.query_info("*").get_size();
|
|
|
+ var file_size = stream.query_info("*").get_size();
|
|
|
+ var to_read = file_size;
|
|
|
while(read < to_read) {
|
|
|
var data = new uint8[uint64.min(increment, to_read - read)];
|
|
|
data.length = (int)stream.read(data);
|
|
|
checksum.update(data, data.length);
|
|
|
read += data.length;
|
|
|
+ file_callback(progress_callback, (int64)read, file_size);
|
|
|
}
|
|
|
|
|
|
var checksum_bytes = new uint8[ChecksumType.SHA512.get_length()];
|
|
@@ -104,6 +231,36 @@ namespace Binman {
|
|
|
return new Invercargill.BinaryData.from_byte_array(checksum_bytes);
|
|
|
}
|
|
|
|
|
|
+ private static void file_callback(FileProgressCallback? cb, int64 current, int64 total) {
|
|
|
+ if(cb != null) {
|
|
|
+ cb(current, total);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ internal static void posix_to_error(int result) throws Error {
|
|
|
+ if(result == 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ throw new Error(GLib.Quark.from_string("posix"), errno, Posix.strerror(errno));
|
|
|
+ }
|
|
|
+
|
|
|
+ internal static void reflink_copy(string src, string dest) throws Error {
|
|
|
+ int pid = Posix.fork();
|
|
|
+ if (pid == 0) {
|
|
|
+ posix_to_error(Posix.execv("/bin/cp", new string[] {"--reflink=always", src, dest}));
|
|
|
+ Posix.exit(Posix.ENOENT);
|
|
|
+ }
|
|
|
+ int status;
|
|
|
+ Posix.waitpid(pid, out status, 0);
|
|
|
+ posix_to_error(status);
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ public class FileDetails {
|
|
|
+ public ManifestFile originating_manifest { get; set; }
|
|
|
+ public ManifestEntry file_description { get; set; }
|
|
|
+ public FileStatus installation_status { get; set; }
|
|
|
}
|
|
|
|
|
|
public enum ApplicationType {
|
|
@@ -126,7 +283,15 @@ namespace Binman {
|
|
|
MISSING = (1 << 1),
|
|
|
BAD_OWNER = (1 << 2),
|
|
|
BAD_GROUP = (1 << 3),
|
|
|
- BAD_MODE = (1 << 4),
|
|
|
- BAD_CONTENT = (1 << 5),
|
|
|
+ BAD_PERMISSIONS = (1 << 4),
|
|
|
+ BAD_TYPE = (1 << 5),
|
|
|
+ BAD_CONTENT = (1 << 6);
|
|
|
+
|
|
|
+ public bool any_set(FileStatus flag) {
|
|
|
+ if(flag == GOOD) {
|
|
|
+ return this == GOOD;
|
|
|
+ }
|
|
|
+ return (this | flag) != 0;
|
|
|
+ }
|
|
|
}
|
|
|
}
|