12345678module exchange910import json211import os12import src.events13import src.remote14import src.storage151617pub struct Scope {18pub:19 files int20 saves int21 objects int22}2324pub struct LocalStrip {25pub:26 files_removed int27 objects_dropped i6428}293031pub struct SendReport {32pub mut:33 records int34 objects int35 event_sent bool36}3738394041pub struct Pending {42mut:43 saves []string44 hashes []string45}4647fn pending_key(path string) string {48 return 'strip_pending:${path}'49}5051525354pub fn read_pending(store &storage.Store, path string) !Pending {55 text := store.state(pending_key(path)) or { return Pending{} }56 note := json2.decode[Pending](text) or {57 return error('the unfinished strip note for ${path} is unreadable; it is kept for recovery')58 }59 recorded := events.strip_events(store)!.filter(events.valid_strip(it) && it.path == path)60 if note.saves.len == 0 || !note.saves.all(fn [recorded] (scope string) bool {61 return recorded.any(it.save_id == scope)62 }) {63 return error('no recorded strip matches the unfinished strip note for ${path}; it is kept for recovery')64 }65 return note66}676869pub fn strip_pending(store &storage.Store, path string) bool {70 return store.state(pending_key(path)) != none71}72737475fn covers(path string, file string) bool {76 return file == path || file.starts_with('${path.trim_right('/')}/')77}78798081pub fn strip_scope(store &storage.Store, path string, only_save string) !Scope {82 mut stmt := store.db.prepare('SELECT COUNT(*), COUNT(DISTINCT save_id), COUNT(DISTINCT hash)83 FROM save_files WHERE ${match_clause(only_save)}')!84 defer {85 stmt.finalize()86 }87 bind_match(stmt, path, only_save)!88 stmt.step()!89 return Scope{90 files: int(stmt.int(0))91 saves: int(stmt.int(1))92 objects: int(stmt.int(2))93 }94}95969798pub fn strip_local(store &storage.Store, path string, only_save string) !LocalStrip {99 store.begin()!100 report := strip_rows(store, path, only_save) or {101 store.rollback()102 return err103 }104 store.end()!105 return report106}107108109pub fn strip_local_batch(store &storage.Store, paths []string, only_save string) ![]LocalStrip {110 store.begin()!111 mut reports := []LocalStrip{}112 for path in paths {113 reports << strip_rows(store, path, only_save) or {114 store.rollback()115 return err116 }117 }118 store.end() or {119 store.rollback()120 return err121 }122 return reports123}124125126127fn strip_rows(store &storage.Store, path string, only_save string) !LocalStrip {128 hashes := stripped_hashes(store, path, only_save)!129 if hashes.len == 0 {130 131 return LocalStrip{}132 }133 removed := remove_path(store, path, only_save)!134 dropped := store.drop_unused(hashes)!135 events.strip(store, only_save, path)!136 137 138 mut note := read_pending(store, path)!139 if only_save !in note.saves {140 note.saves << only_save141 }142 note.hashes << hashes143 store.set_state(pending_key(path), json2.encode(note))!144 return LocalStrip{145 files_removed: removed146 objects_dropped: dropped147 }148}149150151152153154155pub fn send_strip(store &storage.Store, mut backend remote.Backend, path string) !SendReport {156 note := read_pending(store, path)!157 name := repo_name(store, mut backend)!158 mut report := SendReport{}159 mut upload := OfferReport{160 repo: name161 }162 upload_events(store, mut backend, name, mut upload)!163 report.event_sent = upload.events > 0164 mut freed := map[string]bool{}165 for hash in note.hashes {166 freed[hash] = true167 }168 every_save := '' in note.saves169 mut needed := map[string]bool{}170 for key in backend.list('${name}/saves')! {171 remote_object := backend.get(key)!172 if !remote_object.found {173 continue174 }175 record := decode_save(remote_object.body)!176 mut kept := []FileRecord{}177 for file in record.files {178 if (every_save || record.id in note.saves) && covers(path, file.path) {179 freed[file.hash] = true180 continue181 }182 kept << file183 needed[file.hash] = true184 }185 if kept.len == record.files.len {186 continue187 }188 if !backend.put_if_match(key, encode_save(rewrite(record, kept)).bytes(), remote_object.tag)! {189 return error('the remote copy of save ${record.id} moved while stripping — run koh steal, then strip again')190 }191 report.records++192 }193 for hash, _ in freed {194 if hash in needed || store.has_object(hash)! {195 continue196 }197 backend.delete(object_key(name, hash))!198 report.objects++199 }200 store.remove_state(pending_key(path))!201 return report202}203204205206207fn unapplied_strips(store &storage.Store, mut backend remote.Backend, repo string) ![]events.Event {208 mut known := map[string]bool{}209 for event in events.all(store)! {210 known[event.id] = true211 }212 mut strips := []events.Event{}213 for key in backend.list(events_prefix(repo))! {214 if os.base(key) in known {215 continue216 }217 body := backend.get(key)!218 if !body.found {219 continue220 }221 event := decode_event(body.body)!222 if event.kind == events.kind_strip && !ignored_strip(event) {223 strips << event224 }225 }226 return strips227}228229230fn rewrite(record SaveRecord, files []FileRecord) SaveRecord {231 return SaveRecord{232 ...record233 files: files234 }235}236237238fn stripped_hashes(store &storage.Store, path string, only_save string) ![]string {239 mut stmt :=240 store.db.prepare('SELECT DISTINCT hash FROM save_files WHERE ${match_clause(only_save)}')!241 defer {242 stmt.finalize()243 }244 bind_match(stmt, path, only_save)!245 mut hashes := []string{}246 for stmt.step()! {247 hashes << stmt.text(0)248 }249 return hashes250}251252253fn remove_path(store &storage.Store, path string, only_save string) !int {254 mut stmt := store.db.prepare('DELETE FROM save_files WHERE ${match_clause(only_save)}')!255 defer {256 stmt.finalize()257 }258 bind_match(stmt, path, only_save)!259 stmt.step()!260 return store.db.changes()261}262263264265fn match_clause(only_save string) string {266 scope := if only_save == '' { '' } else { ' AND save_id = ?3' }267 return '(path = ?1 OR substr(path, 1, length(?2)) = ?2)${scope}'268}269270271fn bind_match(stmt &storage.Stmt, path string, only_save string) ! {272 stmt.bind_text(1, path)!273 stmt.bind_text(2, '${path.trim_right('/')}/')!274 if only_save != '' {275 stmt.bind_text(3, only_save)!276 }277}