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}66676869pub fn strip_scope(store &storage.Store, path string, only_save string) !Scope {70 mut stmt := store.db.prepare('SELECT COUNT(*), COUNT(DISTINCT save_id), COUNT(DISTINCT hash)71 FROM save_files WHERE ${match_clause(only_save)}')!72 defer {73 stmt.finalize()74 }75 bind_match(stmt, path, only_save)!76 stmt.step()!77 return Scope{78 files: int(stmt.int(0))79 saves: int(stmt.int(1))80 objects: int(stmt.int(2))81 }82}83848586pub fn strip_local(store &storage.Store, path string, only_save string) !LocalStrip {87 store.begin()!88 report := strip_rows(store, path, only_save) or {89 store.rollback()90 return err91 }92 store.end()!93 return report94}959697pub fn strip_local_batch(store &storage.Store, paths []string, only_save string) ![]LocalStrip {98 store.begin()!99 mut reports := []LocalStrip{}100 for path in paths {101 reports << strip_rows(store, path, only_save) or {102 store.rollback()103 return err104 }105 }106 store.end() or {107 store.rollback()108 return err109 }110 return reports111}112113114115fn strip_rows(store &storage.Store, path string, only_save string) !LocalStrip {116 mut hashes := []string{}117 mut stmt :=118 store.db.prepare('SELECT DISTINCT hash FROM save_files WHERE ${match_clause(only_save)}')!119 bind_match(stmt, path, only_save)!120 for stmt.step()! {121 hashes << stmt.text(0)122 }123 stmt.finalize()124 if hashes.len == 0 {125 126 return LocalStrip{}127 }128 removed := remove_path(store, path, only_save)!129 dropped := store.drop_unreferenced_objects()!130 events.strip(store, only_save, path)!131 132 133 mut note := read_pending(store, path)134 if only_save !in note.saves {135 note.saves << only_save136 }137 note.hashes << hashes138 store.set_state(pending_key(path), json2.encode(note))!139 return LocalStrip{140 files_removed: removed141 objects_dropped: dropped142 }143}144145146147148149150pub fn send_strip(store &storage.Store, mut backend remote.Backend, path string) !SendReport {151 name := repo_name(store, mut backend)!152 mut report := SendReport{}153 mut upload := OfferReport{154 repo: name155 }156 upload_events(store, mut backend, name, mut upload)!157 report.event_sent = upload.events > 0158 note := read_pending(store, path)159 mut freed := map[string]bool{}160 for hash in note.hashes {161 freed[hash] = true162 }163 every_save := '' in note.saves164 mut needed := map[string]bool{}165 for key in backend.list('${name}/saves')! {166 remote_object := backend.get(key)!167 if !remote_object.found {168 continue169 }170 record := decode_save(remote_object.body)!171 mut kept := []FileRecord{}172 for file in record.files {173 if (every_save || record.id in note.saves) && covers(path, file.path) {174 freed[file.hash] = true175 continue176 }177 kept << file178 needed[file.hash] = true179 }180 if kept.len == record.files.len {181 continue182 }183 if !backend.put_if_match(key, encode_save(rewrite(record, kept)).bytes(), remote_object.tag)! {184 return error('the remote copy of save ${record.id} moved while stripping — run koh steal, then strip again')185 }186 report.records++187 }188 for hash, _ in freed {189 if hash in needed || store.has_object(hash)! {190 continue191 }192 backend.delete(object_key(name, hash))!193 report.objects++194 }195 store.remove_state(pending_key(path))!196 return report197}198199200201202fn unapplied_strips(store &storage.Store, mut backend remote.Backend, repo string) ![]events.Event {203 mut known := map[string]bool{}204 for event in events.all(store)! {205 known[event.id] = true206 }207 mut strips := []events.Event{}208 for key in backend.list(events_prefix(repo))! {209 if os.base(key) in known {210 continue211 }212 body := backend.get(key)!213 if !body.found {214 continue215 }216 event := decode_event(body.body)!217 if event.kind == events.kind_strip && !ignored_strip(event) {218 strips << event219 }220 }221 return strips222}223224225fn rewrite(record SaveRecord, files []FileRecord) SaveRecord {226 return SaveRecord{227 ...record228 files: files229 }230}231232233fn remove_path(store &storage.Store, path string, only_save string) !int {234 mut stmt := store.db.prepare('DELETE FROM save_files WHERE ${match_clause(only_save)}')!235 defer {236 stmt.finalize()237 }238 bind_match(stmt, path, only_save)!239 stmt.step()!240 return store.db.changes()241}242243244245fn match_clause(only_save string) string {246 scope := if only_save == '' { '' } else { ' AND save_id = ?3' }247 return '(path = ?1 OR substr(path, 1, length(?2)) = ?2)${scope}'248}249250251fn bind_match(stmt &storage.Stmt, path string, only_save string) ! {252 stmt.bind_text(1, path)!253 stmt.bind_text(2, '${path.trim_right('/')}/')!254 if only_save != '' {255 stmt.bind_text(3, only_save)!256 }257}