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}505152535455pub fn read_pending(store &storage.Store, path string) !Pending {56 text := store.state(pending_key(path)) or { return Pending{} }57 note := json2.decode[Pending](text) or {58 return error('the unfinished strip note for ${path} is unreadable; it is kept for recovery')59 }60 if !note.hashes.all(storage.valid_hash(it)) {61 return error('the unfinished strip note for ${path} names something other than an object; it is kept for recovery')62 }63 recorded := events.strip_events(store)!.filter(events.valid_strip(it) && it.path == path)64 if note.saves.len == 0 || !note.saves.all(fn [recorded] (scope string) bool {65 return recorded.any(it.save_id == scope)66 }) {67 return error('no recorded strip matches the unfinished strip note for ${path}; it is kept for recovery')68 }69 return note70}717273pub fn strip_pending(store &storage.Store, path string) bool {74 return store.state(pending_key(path)) != none75}76777879fn covers(path string, file string) bool {80 return file == path || file.starts_with('${path.trim_right('/')}/')81}82838485pub fn strip_scope(store &storage.Store, path string, only_save string) !Scope {86 mut stmt := store.db.prepare('SELECT COUNT(*), COUNT(DISTINCT save_id), COUNT(DISTINCT hash)87 FROM save_files WHERE ${match_clause(only_save)}')!88 defer {89 stmt.finalize()90 }91 bind_match(stmt, path, only_save)!92 stmt.step()!93 return Scope{94 files: int(stmt.int(0))95 saves: int(stmt.int(1))96 objects: int(stmt.int(2))97 }98}99100101102pub fn strip_local(store &storage.Store, path string, only_save string) !LocalStrip {103 store.begin()!104 report := strip_rows(store, path, only_save) or {105 store.rollback()106 return err107 }108 store.end()!109 return report110}111112113pub fn strip_local_batch(store &storage.Store, paths []string, only_save string) ![]LocalStrip {114 store.begin()!115 mut reports := []LocalStrip{}116 for path in paths {117 reports << strip_rows(store, path, only_save) or {118 store.rollback()119 return err120 }121 }122 store.end() or {123 store.rollback()124 return err125 }126 return reports127}128129130131fn strip_rows(store &storage.Store, path string, only_save string) !LocalStrip {132 hashes := stripped_hashes(store, path, only_save)!133 if hashes.len == 0 {134 135 return LocalStrip{}136 }137 removed := remove_path(store, path, only_save)!138 dropped := store.drop_unused(hashes)!139 events.strip(store, only_save, path)!140 141 142 mut note := read_pending(store, path)!143 if only_save !in note.saves {144 note.saves << only_save145 }146 note.hashes << hashes147 store.set_state(pending_key(path), json2.encode(note))!148 return LocalStrip{149 files_removed: removed150 objects_dropped: dropped151 }152}153154155156157158159pub fn send_strip(store &storage.Store, mut backend remote.Backend, path string) !SendReport {160 note := read_pending(store, path)!161 name := repo_name(store, mut backend)!162 mut report := SendReport{}163 mut upload := OfferReport{164 repo: name165 }166 upload_events(store, mut backend, name, mut upload)!167 report.event_sent = upload.events > 0168 mut freed := map[string]bool{}169 for hash in note.hashes {170 freed[hash] = true171 }172 every_save := '' in note.saves173 mut needed := map[string]bool{}174 for key in backend.list('${name}/saves')! {175 remote_object := backend.get(key)!176 if !remote_object.found {177 continue178 }179 record := decode_save(remote_object.body)!180 mut kept := []FileRecord{}181 for file in record.files {182 if (every_save || record.id in note.saves) && covers(path, file.path) {183 freed[file.hash] = true184 continue185 }186 kept << file187 needed[file.hash] = true188 }189 if kept.len == record.files.len {190 continue191 }192 if !backend.put_if_match(key, encode_save(rewrite(record, kept)).bytes(), remote_object.tag)! {193 return error('the remote copy of save ${record.id} moved while stripping — run koh steal, then strip again')194 }195 report.records++196 }197 for hash, _ in freed {198 if hash in needed || store.has_object(hash)! {199 continue200 }201 backend.delete(object_key(name, hash))!202 report.objects++203 }204 store.remove_state(pending_key(path))!205 return report206}207208209210211fn unapplied_strips(store &storage.Store, mut backend remote.Backend, repo string) ![]events.Event {212 mut known := map[string]bool{}213 for event in events.all(store)! {214 known[event.id] = true215 }216 mut strips := []events.Event{}217 for key in backend.list(events_prefix(repo))! {218 if os.base(key) in known {219 continue220 }221 body := backend.get(key)!222 if !body.found {223 continue224 }225 event := decode_event(body.body)!226 if event.kind == events.kind_strip && !ignored_strip(event) {227 strips << event228 }229 }230 return strips231}232233234fn rewrite(record SaveRecord, files []FileRecord) SaveRecord {235 return SaveRecord{236 ...record237 files: files238 }239}240241242fn stripped_hashes(store &storage.Store, path string, only_save string) ![]string {243 mut stmt :=244 store.db.prepare('SELECT DISTINCT hash FROM save_files WHERE ${match_clause(only_save)}')!245 defer {246 stmt.finalize()247 }248 bind_match(stmt, path, only_save)!249 mut hashes := []string{}250 for stmt.step()! {251 hashes << stmt.text(0)252 }253 return hashes254}255256257fn remove_path(store &storage.Store, path string, only_save string) !int {258 mut stmt := store.db.prepare('DELETE FROM save_files WHERE ${match_clause(only_save)}')!259 defer {260 stmt.finalize()261 }262 bind_match(stmt, path, only_save)!263 stmt.step()!264 return store.db.changes()265}266267268269fn match_clause(only_save string) string {270 scope := if only_save == '' { '' } else { ' AND save_id = ?3' }271 return '(path = ?1 OR substr(path, 1, length(?2)) = ?2)${scope}'272}273274275fn bind_match(stmt &storage.Stmt, path string, only_save string) ! {276 stmt.bind_text(1, path)!277 stmt.bind_text(2, '${path.trim_right('/')}/')!278 if only_save != '' {279 stmt.bind_text(3, only_save)!280 }281}