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}555657pub fn strip_pending(store &storage.Store, path string) bool {58 return store.state(pending_key(path)) != none59}60616263fn covers(path string, file string) bool {64 return file == path || file.starts_with('${path.trim_right('/')}/')65}666768pub fn strip_scope(store &storage.Store, path string, only_save string) !Scope {69 mut stmt := store.db.prepare('SELECT COUNT(*), COUNT(DISTINCT save_id), COUNT(DISTINCT hash)70 FROM save_files WHERE ${match_clause(only_save)}')!71 defer {72 stmt.finalize()73 }74 bind_match(stmt, path, only_save)!75 stmt.step()!76 return Scope{77 files: int(stmt.int(0))78 saves: int(stmt.int(1))79 objects: int(stmt.int(2))80 }81}82838485pub fn strip_local(store &storage.Store, path string, only_save string) !LocalStrip {86 store.begin()!87 report := strip_rows(store, path, only_save) or {88 store.rollback()89 return err90 }91 store.end()!92 return report93}94959697fn strip_rows(store &storage.Store, path string, only_save string) !LocalStrip {98 mut hashes := []string{}99 mut stmt :=100 store.db.prepare('SELECT DISTINCT hash FROM save_files WHERE ${match_clause(only_save)}')!101 bind_match(stmt, path, only_save)!102 for stmt.step()! {103 hashes << stmt.text(0)104 }105 stmt.finalize()106 if hashes.len == 0 {107 108 return LocalStrip{}109 }110 removed := remove_path(store, path, only_save)!111 dropped := store.drop_unreferenced_objects()!112 events.strip(store, only_save, path)!113 114 115 mut note := read_pending(store, path)116 if only_save !in note.saves {117 note.saves << only_save118 }119 note.hashes << hashes120 store.set_state(pending_key(path), json2.encode(note))!121 return LocalStrip{122 files_removed: removed123 objects_dropped: dropped124 }125}126127128129130131132pub fn send_strip(store &storage.Store, mut backend remote.Backend, path string) !SendReport {133 name := repo_name(store, mut backend)!134 mut report := SendReport{}135 mut upload := OfferReport{136 repo: name137 }138 upload_events(store, mut backend, name, mut upload)!139 report.event_sent = upload.events > 0140 note := read_pending(store, path)141 mut freed := map[string]bool{}142 for hash in note.hashes {143 freed[hash] = true144 }145 every_save := '' in note.saves146 mut needed := map[string]bool{}147 for key in backend.list('${name}/saves')! {148 remote_object := backend.get(key)!149 if !remote_object.found {150 continue151 }152 record := decode_save(remote_object.body)!153 mut kept := []FileRecord{}154 for file in record.files {155 if (every_save || record.id in note.saves) && covers(path, file.path) {156 freed[file.hash] = true157 continue158 }159 kept << file160 needed[file.hash] = true161 }162 if kept.len == record.files.len {163 continue164 }165 if !backend.put_if_match(key, encode_save(rewrite(record, kept)).bytes(), remote_object.tag)! {166 return error('the remote copy of save ${record.id} moved while stripping — run koh steal, then strip again')167 }168 report.records++169 }170 for hash, _ in freed {171 if hash in needed || store.has_object(hash)! {172 continue173 }174 backend.delete(object_key(name, hash))!175 report.objects++176 }177 store.remove_state(pending_key(path))!178 return report179}180181182183184fn unapplied_strips(store &storage.Store, mut backend remote.Backend, repo string) ![]events.Event {185 mut known := map[string]bool{}186 for event in events.all(store)! {187 known[event.id] = true188 }189 mut strips := []events.Event{}190 for key in backend.list(events_prefix(repo))! {191 if os.base(key) in known {192 continue193 }194 body := backend.get(key)!195 if !body.found {196 continue197 }198 event := decode_event(body.body)!199 if event.kind == events.kind_strip && !ignored_strip(event) {200 strips << event201 }202 }203 return strips204}205206207fn rewrite(record SaveRecord, files []FileRecord) SaveRecord {208 return SaveRecord{209 ...record210 files: files211 }212}213214215fn remove_path(store &storage.Store, path string, only_save string) !int {216 mut stmt := store.db.prepare('DELETE FROM save_files WHERE ${match_clause(only_save)}')!217 defer {218 stmt.finalize()219 }220 bind_match(stmt, path, only_save)!221 stmt.step()!222 return store.db.changes()223}224225226227fn match_clause(only_save string) string {228 scope := if only_save == '' { '' } else { ' AND save_id = ?3' }229 return '(path = ?1 OR substr(path, 1, length(?2)) = ?2)${scope}'230}231232233fn bind_match(stmt &storage.Stmt, path string, only_save string) ! {234 stmt.bind_text(1, path)!235 stmt.bind_text(2, '${path.trim_right('/')}/')!236 if only_save != '' {237 stmt.bind_text(3, only_save)!238 }239}