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}95969798fn strip_rows(store &storage.Store, path string, only_save string) !LocalStrip {99 mut hashes := []string{}100 mut stmt :=101 store.db.prepare('SELECT DISTINCT hash FROM save_files WHERE ${match_clause(only_save)}')!102 bind_match(stmt, path, only_save)!103 for stmt.step()! {104 hashes << stmt.text(0)105 }106 stmt.finalize()107 if hashes.len == 0 {108 109 return LocalStrip{}110 }111 removed := remove_path(store, path, only_save)!112 dropped := store.drop_unreferenced_objects()!113 events.strip(store, only_save, path)!114 115 116 mut note := read_pending(store, path)117 if only_save !in note.saves {118 note.saves << only_save119 }120 note.hashes << hashes121 store.set_state(pending_key(path), json2.encode(note))!122 return LocalStrip{123 files_removed: removed124 objects_dropped: dropped125 }126}127128129130131132133pub fn send_strip(store &storage.Store, mut backend remote.Backend, path string) !SendReport {134 name := repo_name(store, mut backend)!135 mut report := SendReport{}136 mut upload := OfferReport{137 repo: name138 }139 upload_events(store, mut backend, name, mut upload)!140 report.event_sent = upload.events > 0141 note := read_pending(store, path)142 mut freed := map[string]bool{}143 for hash in note.hashes {144 freed[hash] = true145 }146 every_save := '' in note.saves147 mut needed := map[string]bool{}148 for key in backend.list('${name}/saves')! {149 remote_object := backend.get(key)!150 if !remote_object.found {151 continue152 }153 record := decode_save(remote_object.body)!154 mut kept := []FileRecord{}155 for file in record.files {156 if (every_save || record.id in note.saves) && covers(path, file.path) {157 freed[file.hash] = true158 continue159 }160 kept << file161 needed[file.hash] = true162 }163 if kept.len == record.files.len {164 continue165 }166 if !backend.put_if_match(key, encode_save(rewrite(record, kept)).bytes(), remote_object.tag)! {167 return error('the remote copy of save ${record.id} moved while stripping — run koh steal, then strip again')168 }169 report.records++170 }171 for hash, _ in freed {172 if hash in needed || store.has_object(hash)! {173 continue174 }175 backend.delete(object_key(name, hash))!176 report.objects++177 }178 store.remove_state(pending_key(path))!179 return report180}181182183184185fn unapplied_strips(store &storage.Store, mut backend remote.Backend, repo string) ![]events.Event {186 mut known := map[string]bool{}187 for event in events.all(store)! {188 known[event.id] = true189 }190 mut strips := []events.Event{}191 for key in backend.list(events_prefix(repo))! {192 if os.base(key) in known {193 continue194 }195 body := backend.get(key)!196 if !body.found {197 continue198 }199 event := decode_event(body.body)!200 if event.kind == events.kind_strip && !ignored_strip(event) {201 strips << event202 }203 }204 return strips205}206207208fn rewrite(record SaveRecord, files []FileRecord) SaveRecord {209 return SaveRecord{210 ...record211 files: files212 }213}214215216fn remove_path(store &storage.Store, path string, only_save string) !int {217 mut stmt := store.db.prepare('DELETE FROM save_files WHERE ${match_clause(only_save)}')!218 defer {219 stmt.finalize()220 }221 bind_match(stmt, path, only_save)!222 stmt.step()!223 return store.db.changes()224}225226227228fn match_clause(only_save string) string {229 scope := if only_save == '' { '' } else { ' AND save_id = ?3' }230 return '(path = ?1 OR substr(path, 1, length(?2)) = ?2)${scope}'231}232233234fn bind_match(stmt &storage.Stmt, path string, only_save string) ! {235 stmt.bind_text(1, path)!236 stmt.bind_text(2, '${path.trim_right('/')}/')!237 if only_save != '' {238 stmt.bind_text(3, only_save)!239 }240}