///|
pub(all) enum OperationStatus {
Moved
Restored
Failed
Skipped
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct OperationItem {
source : String
destination : String
size : Int64
status : OperationStatus
message : String
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct OperationManifest {
format_version : Int
root : String
quarantine_root : String
created_at : Int64
items : Array[OperationItem]
} derive(Eq, Debug, ToJson, FromJson)
///|
fn native_join(base : String, relative : String) -> String {
if relative == "" {
base
} else {
base + "/" + relative
}
}
///|
async fn ensure_relative_parent(base : String, relative : String) -> Unit {
let parent = parent_path(relative)
if parent == "" {
return
}
let mut current = base
for part in parent.split("/") {
current = current + "/" + part.to_owned()
if !@fs.exists(current) {
@fs.mkdir(current)
}
}
}
///|
pub async fn apply_cleanup_plan(plan : CleanupPlan) -> OperationManifest {
if !@fs.exists(plan.quarantine_root) {
@fs.mkdir(plan.quarantine_root, recursive=true)
}
let results : Array[OperationItem] = []
for item in plan.items {
let source = native_join(plan.root, item.source)
let destination = native_join(plan.quarantine_root, item.destination)
if !@fs.exists(source) {
results.push(OperationItem::{
source: item.source,
destination: item.destination,
size: item.expected_size,
status: Skipped,
message: "Source no longer exists.",
})
continue
}
if @fs.exists(destination) {
results.push(OperationItem::{
source: item.source,
destination: item.destination,
size: item.expected_size,
status: Skipped,
message: "Quarantine destination already exists.",
})
continue
}
try {
ensure_relative_parent(plan.quarantine_root, item.destination)
@fs.rename(source, destination, replace=false)
results.push(OperationItem::{
source: item.source,
destination: item.destination,
size: item.expected_size,
status: Moved,
message: "Moved to quarantine.",
})
} catch {
error =>
results.push(OperationItem::{
source: item.source,
destination: item.destination,
size: item.expected_size,
status: Failed,
message: @debug.to_string(error),
})
}
}
OperationManifest::{
format_version: 1,
root: plan.root,
quarantine_root: plan.quarantine_root,
created_at: plan.created_at,
items: results,
}
}
///|
pub async fn restore_operation(
operation : OperationManifest,
) -> OperationManifest {
let results : Array[OperationItem] = []
let mut index = operation.items.length() - 1
while index >= 0 {
let item = operation.items[index]
if item.status != Moved {
results.push({
..item,
status: Skipped,
message: "Item was not moved by this operation.",
})
index -= 1
continue
}
let source = native_join(operation.quarantine_root, item.destination)
let destination = native_join(operation.root, item.source)
if !@fs.exists(source) || @fs.exists(destination) {
let message = if !@fs.exists(source) {
"Quarantined source no longer exists."
} else {
"Original destination already exists."
}
results.push({ ..item, status: Skipped, message, })
index -= 1
continue
}
try {
ensure_relative_parent(operation.root, item.source)
@fs.rename(source, destination, replace=false)
results.push({
..item,
status: Restored,
message: "Restored to original path.",
})
} catch {
error =>
results.push({
..item,
status: Failed,
message: @debug.to_string(error),
})
}
index -= 1
}
{ ..operation, items: results, }
}
///|
pub fn operation_to_json(operation : OperationManifest) -> String {
ToJson::to_json(operation).stringify(indent=2)
}
///|
pub fn operation_from_json(source : String) -> OperationManifest raise {
let operation : OperationManifest = @json.from_json(@json.parse(source))
if operation.format_version != 1 {
raise Failure("Unsupported operation manifest version")
}
operation
}
///|
pub async fn write_operation(
path : String,
operation : OperationManifest,
) -> Unit {
@fs.write_file(path, operation_to_json(operation))
}
///|
pub async fn read_operation(path : String) -> OperationManifest {
operation_from_json(@fs.read_file(path).text())
}
///|
pub fn OperationManifest::moved_count(self : OperationManifest) -> Int {
self.items.filter(fn(item) { item.status == Moved }).length()
}
///|
pub fn OperationManifest::restored_count(self : OperationManifest) -> Int {
self.items.filter(fn(item) { item.status == Restored }).length()
}
///|
pub fn OperationManifest::failed_count(self : OperationManifest) -> Int {
self.items.filter(fn(item) { item.status == Failed }).length()
}