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.event_sent || report.records + report.objects > 0 {160 done << '${if report.event_sent { 'sent its event, ' } else { '' }}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 defer {174 report.event_sent = upload.events > 0175 }176 upload_events(store, mut backend, name, mut upload)!177 mut freed := map[string]bool{}178 for hash in note.hashes {179 freed[hash] = true180 }181 every_save := '' in note.saves182 mut needed := map[string]bool{}183 for key in backend.list('${name}/saves')! {184 remote_object := backend.get(key)!185 if !remote_object.found {186 continue187 }188 record := decode_save(remote_object.body)!189 mut kept := []FileRecord{}190 mut dropped := []string{}191 for file in record.files {192 if (every_save || record.id in note.saves) && covers(path, file.path) {193 freed[file.hash] = true194 dropped << file.hash195 continue196 }197 kept << file198 needed[file.hash] = true199 }200 if kept.len == record.files.len {201 continue202 }203 204 205 queue_strip(store, path, []string{}, dropped)!206 if !backend.put_if_match(key, encode_save(rewrite(record, kept)).bytes(), remote_object.tag)! {207 return error('the remote copy of save ${record.id} moved while stripping — run koh steal, then strip again')208 }209 report.records++210 }211 for hash, _ in freed {212 if hash in needed || !storage.valid_hash(hash) || store.has_object(hash)! {213 continue214 }215 backend.delete(object_key(name, hash))!216 report.objects++217 }218 store.remove_state(pending_key(path))!219}220221222223pub fn requeue_strip(store &storage.Store, path string) ! {224 recorded := events.strip_events(store)!.filter(events.valid_strip(it) && it.path == path)225 if recorded.len == 0 {226 return error('this repo has no recorded strip of ${path}; run koh steal, then koh doctor')227 }228 queue_strip(store, path, recorded.map(it.save_id), []string{})!229}230231232233fn queue_strip(store &storage.Store, path string, scopes []string, hashes []string) ! {234 mut note := read_pending(store, path)!235 count := note.saves.len + note.hashes.len236 note.saves << scopes.filter(it !in note.saves)237 note.hashes << hashes.filter(it !in note.hashes && storage.valid_hash(it))238 if note.saves.len + note.hashes.len > count {239 store.set_state(pending_key(path), json2.encode(note))!240 }241}242243244245246fn unapplied_strips(store &storage.Store, mut backend remote.Backend, repo string) ![]events.Event {247 mut known := map[string]bool{}248 for event in events.all(store)! {249 known[event.id] = true250 }251 mut strips := []events.Event{}252 for key in backend.list(events_prefix(repo))! {253 if os.base(key) in known {254 continue255 }256 body := backend.get(key)!257 if !body.found {258 continue259 }260 event := decode_event(body.body)!261 if events.valid_strip(event) && !ignored_strip(event) {262 strips << event263 }264 }265 return strips266}267268269fn rewrite(record SaveRecord, files []FileRecord) SaveRecord {270 return SaveRecord{271 ...record272 files: files273 }274}275276277fn stripped_hashes(store &storage.Store, path string, only_save string) ![]string {278 mut stmt :=279 store.db.prepare('SELECT DISTINCT hash FROM save_files WHERE ${match_clause(only_save)}')!280 defer {281 stmt.finalize()282 }283 bind_match(stmt, path, only_save)!284 mut hashes := []string{}285 for stmt.step()! {286 hashes << stmt.text(0)287 }288 return hashes289}290291292fn remove_path(store &storage.Store, path string, only_save string) !int {293 mut stmt := store.db.prepare('DELETE FROM save_files WHERE ${match_clause(only_save)}')!294 defer {295 stmt.finalize()296 }297 bind_match(stmt, path, only_save)!298 stmt.step()!299 return store.db.changes()300}301302303304fn match_clause(only_save string) string {305 scope := if only_save == '' { '' } else { ' AND save_id = ?3' }306 return '(path = ?1 OR substr(path, 1, length(?2)) = ?2)${scope}'307}308309310fn bind_match(stmt &storage.Stmt, path string, only_save string) ! {311 stmt.bind_text(1, path)!312 stmt.bind_text(2, '${path.trim_right('/')}/')!313 if only_save != '' {314 stmt.bind_text(3, only_save)!315 }316}