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 || store.paths_of_object(hash)!.len > 0 {214 continue215 }216 backend.delete(object_key(name, hash))!217 report.objects++218 }219 store.remove_state(pending_key(path))!220}221222223224pub fn requeue_strip(store &storage.Store, path string) ! {225 recorded := events.strip_events(store)!.filter(events.valid_strip(it) && it.path == path)226 if recorded.len == 0 {227 return error('this repo has no recorded strip of ${path}; run koh steal, then koh doctor')228 }229 queue_strip(store, path, recorded.map(it.save_id), []string{})!230}231232233234fn queue_strip(store &storage.Store, path string, scopes []string, hashes []string) ! {235 mut note := read_pending(store, path)!236 count := note.saves.len + note.hashes.len237 note.saves << scopes.filter(it !in note.saves)238 note.hashes << hashes.filter(it !in note.hashes && storage.valid_hash(it))239 if note.saves.len + note.hashes.len > count {240 store.set_state(pending_key(path), json2.encode(note))!241 }242}243244245246247fn unapplied_strips(store &storage.Store, mut backend remote.Backend, repo string) ![]events.Event {248 mut known := map[string]bool{}249 for event in events.all(store)! {250 known[event.id] = true251 }252 mut strips := []events.Event{}253 for key in backend.list(events_prefix(repo))! {254 if os.base(key) in known {255 continue256 }257 body := backend.get(key)!258 if !body.found {259 continue260 }261 event := decode_event(body.body)!262 if events.valid_strip(event) && !ignored_strip(event) {263 strips << event264 }265 }266 return strips267}268269270fn rewrite(record SaveRecord, files []FileRecord) SaveRecord {271 return SaveRecord{272 ...record273 files: files274 }275}276277278fn stripped_hashes(store &storage.Store, path string, only_save string) ![]string {279 mut stmt :=280 store.db.prepare('SELECT DISTINCT hash FROM save_files WHERE ${match_clause(only_save)}')!281 defer {282 stmt.finalize()283 }284 bind_match(stmt, path, only_save)!285 mut hashes := []string{}286 for stmt.step()! {287 hashes << stmt.text(0)288 }289 return hashes290}291292293fn remove_path(store &storage.Store, path string, only_save string) !int {294 mut stmt := store.db.prepare('DELETE FROM save_files WHERE ${match_clause(only_save)}')!295 defer {296 stmt.finalize()297 }298 bind_match(stmt, path, only_save)!299 stmt.step()!300 return store.db.changes()301}302303304305fn match_clause(only_save string) string {306 scope := if only_save == '' { '' } else { ' AND save_id = ?3' }307 return '(path = ?1 OR substr(path, 1, length(?2)) = ?2)${scope}'308}309310311fn bind_match(stmt &storage.Stmt, path string, only_save string) ! {312 stmt.bind_text(1, path)!313 stmt.bind_text(2, '${path.trim_right('/')}/')!314 if only_save != '' {315 stmt.bind_text(3, only_save)!316 }317}