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}3738394041struct Pending {42mut:43 saves []string44 hashes []string45}4647fn pending_key(path string) string {48 return 'strip_pending:${path}'49}5051fn read_pending(store &storage.Store, path string) Pending {52 text := store.state(pending_key(path)) or { return Pending{} }53 return json2.decode[Pending](text) or { Pending{} }54}5556pub fn strip_pending(store &storage.Store, path string) bool {57 return store.state(pending_key(path)) != none58}59606162fn covers(path string, file string) bool {63 return file == path || file.starts_with('${path.trim_right('/')}/')64}6566pub fn strip_scope(store &storage.Store, path string, only_save string) !Scope {67 mut stmt := store.db.prepare('SELECT COUNT(*), COUNT(DISTINCT save_id), COUNT(DISTINCT hash)68 FROM save_files WHERE ${match_clause(only_save)}')!69 defer {70 stmt.finalize()71 }72 bind_match(stmt, path, only_save)!73 stmt.step()!74 return Scope{75 files: int(stmt.int(0))76 saves: int(stmt.int(1))77 objects: int(stmt.int(2))78 }79}80818283pub fn strip_local(store &storage.Store, path string, only_save string) !LocalStrip {84 store.begin()!85 report := strip_rows(store, path, only_save) or {86 store.rollback()87 return err88 }89 store.end()!90 return report91}9293fn strip_rows(store &storage.Store, path string, only_save string) !LocalStrip {94 mut hashes := []string{}95 mut stmt :=96 store.db.prepare('SELECT DISTINCT hash FROM save_files WHERE ${match_clause(only_save)}')!97 bind_match(stmt, path, only_save)!98 for stmt.step()! {99 hashes << stmt.text(0)100 }101 stmt.finalize()102 if hashes.len == 0 {103 104 return LocalStrip{}105 }106 removed := remove_path(store, path, only_save)!107 dropped := store.drop_unreferenced_objects()!108 events.strip(store, only_save, path)!109 110 111 mut note := read_pending(store, path)112 if only_save !in note.saves {113 note.saves << only_save114 }115 note.hashes << hashes116 store.set_state(pending_key(path), json2.encode(note))!117 return LocalStrip{118 files_removed: removed119 objects_dropped: dropped120 }121}122123124125126127128pub fn send_strip(store &storage.Store, mut backend remote.Backend, path string) !SendReport {129 name := repo_name(store, mut backend)!130 mut report := SendReport{}131 mut upload := OfferReport{132 repo: name133 }134 upload_events(store, mut backend, name, mut upload)!135 report.event_sent = upload.events > 0136 note := read_pending(store, path)137 mut freed := map[string]bool{}138 for hash in note.hashes {139 freed[hash] = true140 }141 every_save := '' in note.saves142 mut needed := map[string]bool{}143 for key in backend.list('${name}/saves')! {144 remote_object := backend.get(key)!145 if !remote_object.found {146 continue147 }148 record := decode_save(remote_object.body)!149 mut kept := []FileRecord{}150 for file in record.files {151 if (every_save || record.id in note.saves) && covers(path, file.path) {152 freed[file.hash] = true153 continue154 }155 kept << file156 needed[file.hash] = true157 }158 if kept.len == record.files.len {159 continue160 }161 if !backend.put_if_match(key, encode_save(rewrite(record, kept)).bytes(), remote_object.tag)! {162 return error('the remote copy of save ${record.id} moved while stripping — run koh steal, then strip again')163 }164 report.records++165 }166 for hash, _ in freed {167 if hash in needed || store.has_object(hash)! {168 continue169 }170 backend.delete(object_key(name, hash))!171 report.objects++172 }173 store.remove_state(pending_key(path))!174 return report175}176177178179fn unapplied_strips(store &storage.Store, mut backend remote.Backend, repo string) ![]events.Event {180 mut known := map[string]bool{}181 for event in events.all(store)! {182 known[event.id] = true183 }184 mut strips := []events.Event{}185 for key in backend.list(events_prefix(repo))! {186 if os.base(key) in known {187 continue188 }189 body := backend.get(key)!190 if !body.found {191 continue192 }193 event := decode_event(body.body)!194 if event.kind == events.kind_strip && !ignored_strip(event) {195 strips << event196 }197 }198 return strips199}200201202fn rewrite(record SaveRecord, files []FileRecord) SaveRecord {203 return SaveRecord{204 ...record205 files: files206 }207}208209fn remove_path(store &storage.Store, path string, only_save string) !int {210 mut stmt := store.db.prepare('DELETE FROM save_files WHERE ${match_clause(only_save)}')!211 defer {212 stmt.finalize()213 }214 bind_match(stmt, path, only_save)!215 stmt.step()!216 return store.db.changes()217}218219220221fn match_clause(only_save string) string {222 scope := if only_save == '' { '' } else { ' AND save_id = ?3' }223 return '(path = ?1 OR substr(path, 1, length(?2)) = ?2)${scope}'224}225226fn bind_match(stmt &storage.Stmt, path string, only_save string) ! {227 stmt.bind_text(1, path)!228 stmt.bind_text(2, '${path.trim_right('/')}/')!229 if only_save != '' {230 stmt.bind_text(3, only_save)!231 }232}