12345678module exchange910import json211import os12import src.events13import src.fmt14import src.remote15import src.storage161718pub struct Scope {19pub:20 files int21 saves int22 objects int23}2425pub struct LocalStrip {26pub:27 files_removed int28 objects_dropped i6429}303132pub struct SendReport {33pub mut:34 records int35 objects int36 event_sent bool37}3839404142pub struct Pending {43mut:44 saves []string45 hashes []string46}4748fn pending_key(path string) string {49 return 'strip_pending:${path}'50}515253545556pub fn read_pending(store &storage.Store, path string) !Pending {57 text := store.state(pending_key(path)) or { return Pending{} }58 note := json2.decode[Pending](text) or {59 return error('the unfinished strip note for ${path} is unreadable; it is kept for recovery')60 }61 if !note.hashes.all(storage.valid_hash(it)) {62 return error('the unfinished strip note for ${path} names something other than an object; it is kept for recovery')63 }64 recorded := events.strip_events(store)!.filter(events.valid_strip(it) && it.path == path)65 if note.saves.len == 0 || !note.saves.all(fn [recorded] (scope string) bool {66 return recorded.any(it.save_id == scope)67 }) {68 return error('no recorded strip matches the unfinished strip note for ${path}; it is kept for recovery, and koh steal brings a strip made elsewhere')69 }70 return note71}727374pub fn strip_pending(store &storage.Store, path string) bool {75 return store.state(pending_key(path)) != none76}77787980fn covers(path string, file string) bool {81 return file == path || file.starts_with('${path.trim_right('/')}/')82}83848586pub fn strip_scope(store &storage.Store, path string, only_save string) !Scope {87 mut stmt := store.db.prepare('SELECT COUNT(*), COUNT(DISTINCT save_id), COUNT(DISTINCT hash)88 FROM save_files WHERE ${match_clause(only_save)}')!89 defer {90 stmt.finalize()91 }92 bind_match(stmt, path, only_save)!93 stmt.step()!94 return Scope{95 files: int(stmt.int(0))96 saves: int(stmt.int(1))97 objects: int(stmt.int(2))98 }99}100101102103pub fn strip_local(store &storage.Store, path string, only_save string) !LocalStrip {104 store.begin()!105 report := strip_rows(store, path, only_save) or {106 store.rollback()107 return err108 }109 store.end()!110 return report111}112113114pub fn strip_local_batch(store &storage.Store, paths []string, only_save string) ![]LocalStrip {115 store.begin()!116 mut reports := []LocalStrip{}117 for path in paths {118 reports << strip_rows(store, path, only_save) or {119 store.rollback()120 return err121 }122 }123 store.end() or {124 store.rollback()125 return err126 }127 return reports128}129130131132fn strip_rows(store &storage.Store, path string, only_save string) !LocalStrip {133 hashes := stripped_hashes(store, path, only_save)!134 if hashes.len == 0 {135 136 return LocalStrip{}137 }138 removed := remove_path(store, path, only_save)!139 dropped := store.drop_unused(hashes)!140 events.strip(store, only_save, path)!141 142 143 queue_strip(store, path, [only_save], hashes)!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 mut report := SendReport{}157 send_steps(store, mut backend, path, mut report) or {158 mut done := []string{}159 if report.records + report.objects > 0 {160 done << 'rewrote ${fmt.plural(report.records, 'remote record')} and deleted ${fmt.plural(report.objects, 'remote object')} for ${path}'161 }162 return partway(done, err)163 }164 return report165}166167fn send_steps(store &storage.Store, mut backend remote.Backend, path string, mut report SendReport) ! {168 note := read_pending(store, path)!169 name := repo_name(store, mut backend)!170 mut upload := OfferReport{171 repo: name172 }173 upload_events(store, mut backend, name, mut upload)!174 report.event_sent = upload.events > 0175 mut freed := map[string]bool{}176 for hash in note.hashes {177 freed[hash] = true178 }179 every_save := '' in note.saves180 mut needed := map[string]bool{}181 for key in backend.list('${name}/saves')! {182 remote_object := backend.get(key)!183 if !remote_object.found {184 continue185 }186 record := decode_save(remote_object.body)!187 mut kept := []FileRecord{}188 mut dropped := []string{}189 for file in record.files {190 if (every_save || record.id in note.saves) && covers(path, file.path) {191 freed[file.hash] = true192 dropped << file.hash193 continue194 }195 kept << file196 needed[file.hash] = true197 }198 if kept.len == record.files.len {199 continue200 }201 202 203 queue_strip(store, path, []string{}, dropped)!204 if !backend.put_if_match(key, encode_save(rewrite(record, kept)).bytes(), remote_object.tag)! {205 return error('the remote copy of save ${record.id} moved while stripping — run koh steal, then strip again')206 }207 report.records++208 }209 for hash, _ in freed {210 if hash in needed || !storage.valid_hash(hash) || store.has_object(hash)! {211 continue212 }213 backend.delete(object_key(name, hash))!214 report.objects++215 }216 store.remove_state(pending_key(path))!217}218219220221pub fn requeue_strip(store &storage.Store, path string) ! {222 recorded := events.strip_events(store)!.filter(events.valid_strip(it) && it.path == path)223 if recorded.len == 0 {224 return error('this repo has no recorded strip of ${path}; run koh steal, then koh doctor')225 }226 queue_strip(store, path, recorded.map(it.save_id), []string{})!227}228229230231fn queue_strip(store &storage.Store, path string, scopes []string, hashes []string) ! {232 mut note := read_pending(store, path)!233 count := note.saves.len + note.hashes.len234 note.saves << scopes.filter(it !in note.saves)235 note.hashes << hashes.filter(it !in note.hashes && storage.valid_hash(it))236 if note.saves.len + note.hashes.len > count {237 store.set_state(pending_key(path), json2.encode(note))!238 }239}240241242243244fn unapplied_strips(store &storage.Store, mut backend remote.Backend, repo string) ![]events.Event {245 mut known := map[string]bool{}246 for event in events.all(store)! {247 known[event.id] = true248 }249 mut strips := []events.Event{}250 for key in backend.list(events_prefix(repo))! {251 if os.base(key) in known {252 continue253 }254 body := backend.get(key)!255 if !body.found {256 continue257 }258 event := decode_event(body.body)!259 if events.valid_strip(event) && !ignored_strip(event) {260 strips << event261 }262 }263 return strips264}265266267fn rewrite(record SaveRecord, files []FileRecord) SaveRecord {268 return SaveRecord{269 ...record270 files: files271 }272}273274275fn stripped_hashes(store &storage.Store, path string, only_save string) ![]string {276 mut stmt :=277 store.db.prepare('SELECT DISTINCT hash FROM save_files WHERE ${match_clause(only_save)}')!278 defer {279 stmt.finalize()280 }281 bind_match(stmt, path, only_save)!282 mut hashes := []string{}283 for stmt.step()! {284 hashes << stmt.text(0)285 }286 return hashes287}288289290fn remove_path(store &storage.Store, path string, only_save string) !int {291 mut stmt := store.db.prepare('DELETE FROM save_files WHERE ${match_clause(only_save)}')!292 defer {293 stmt.finalize()294 }295 bind_match(stmt, path, only_save)!296 stmt.step()!297 return store.db.changes()298}299300301302fn match_clause(only_save string) string {303 scope := if only_save == '' { '' } else { ' AND save_id = ?3' }304 return '(path = ?1 OR substr(path, 1, length(?2)) = ?2)${scope}'305}306307308fn bind_match(stmt &storage.Stmt, path string, only_save string) ! {309 stmt.bind_text(1, path)!310 stmt.bind_text(2, '${path.trim_right('/')}/')!311 if only_save != '' {312 stmt.bind_text(3, only_save)!313 }314}