12345678module storage910import compress.zlib11import crypto.blake31213pub const max_file_size = 512 * 1024 * 10241415pub fn hash_hex(data []u8) string {16 return blake3.sum256(data).hex()17}18192021pub fn (s &Store) put_object(data []u8) !string {22 hash := hash_hex(data)23 if s.has_object(hash)! {24 return hash25 }26 packed := zlib.compress(data)!27 mut stmt := s.db.prepare('INSERT OR IGNORE INTO objects (hash, size, data) VALUES (?, ?, ?)')!28 defer {29 stmt.finalize()30 }31 stmt.bind_text(1, hash)!32 stmt.bind_int(2, i64(data.len))!33 stmt.bind_blob(3, packed)!34 stmt.step()!35 return hash36}37383940pub fn (s &Store) object_blob(hash string) ![]u8 {41 mut stmt := s.db.prepare('SELECT data FROM objects WHERE hash = ?')!42 defer {43 stmt.finalize()44 }45 stmt.bind_text(1, hash)!46 if !stmt.step()! {47 return error('object ${hash} is missing')48 }49 return stmt.blob(0)50}5152535455pub fn (s &Store) put_object_blob(hash string, blob []u8) ! {56 if s.has_object(hash)! {57 return58 }59 data := zlib.decompress(blob) or { return error('object ${hash} is damaged: ${err.msg()}') }60 if hash_hex(data) != hash {61 return error('object ${hash} is damaged: it does not match its hash')62 }63 mut stmt := s.db.prepare('INSERT OR IGNORE INTO objects (hash, size, data) VALUES (?, ?, ?)')!64 defer {65 stmt.finalize()66 }67 stmt.bind_text(1, hash)!68 stmt.bind_int(2, i64(data.len))!69 stmt.bind_blob(3, blob)!70 stmt.step()!71}7273pub fn (s &Store) has_object(hash string) !bool {74 mut stmt := s.db.prepare('SELECT 1 FROM objects WHERE hash = ?')!75 defer {76 stmt.finalize()77 }78 stmt.bind_text(1, hash)!79 return stmt.step()!80}81828384pub fn (s &Store) get_object(hash string) ![]u8 {85 mut stmt := s.db.prepare('SELECT data FROM objects WHERE hash = ?')!86 defer {87 stmt.finalize()88 }89 stmt.bind_text(1, hash)!90 if !stmt.step()! {91 return error('object ${hash} is missing')92 }93 packed := stmt.blob(0)94 data := zlib.decompress(packed)!95 if hash_hex(data) != hash {96 return error('object ${hash} does not match its hash')97 }98 return data99}100101pub fn (s &Store) object_size(hash string) !i64 {102 mut stmt := s.db.prepare('SELECT size FROM objects WHERE hash = ?')!103 defer {104 stmt.finalize()105 }106 stmt.bind_text(1, hash)!107 if !stmt.step()! {108 return error('object ${hash} is missing')109 }110 return stmt.int(0)111}112113pub fn (s &Store) object_count() !i64 {114 mut stmt := s.db.prepare('SELECT COUNT(*) FROM objects')!115 defer {116 stmt.finalize()117 }118 if !stmt.step()! {119 return 0120 }121 return stmt.int(0)122}123124125126pub fn (s &Store) drop_unreferenced_objects() !i64 {127 before := s.object_count()!128 mut stmt := s.db.prepare('DELETE FROM objects WHERE hash NOT IN (SELECT hash FROM save_files)')!129 defer {130 stmt.finalize()131 }132 stmt.step()!133 after := s.object_count()!134 return before - after135}136137138139140pub fn (s &Store) drop_unused(hashes []string) !i64 {141 mut stmt := s.db.prepare('DELETE FROM objects WHERE hash = ?1142 AND NOT EXISTS (SELECT 1 FROM save_files WHERE hash = ?1)')!143 defer {144 stmt.finalize()145 }146 mut dropped := i64(0)147 for hash in hashes {148 stmt.reset()149 stmt.bind_text(1, hash)!150 stmt.step()!151 dropped += s.db.changes()152 }153 return dropped154}155156157pub struct ObjectProblem {158pub:159 hash string160 detail string161}162163164pub fn (s &Store) check_objects() ![]ObjectProblem {165 mut problems := []ObjectProblem{}166 mut stmt := s.db.prepare('SELECT hash FROM objects ORDER BY hash')!167 defer {168 stmt.finalize()169 }170 mut hashes := []string{}171 for stmt.step()! {172 hashes << stmt.text(0)173 }174 for hash in hashes {175 data := s.get_object(hash) or {176 problems << ObjectProblem{hash, 'object ${hash}: ${err.msg()}'}177 continue178 }179 if s.object_size(hash)! != data.len {180 problems << ObjectProblem{hash, 'object ${hash}: stored size does not match ${data.len} bytes'}181 }182 }183 return problems184}185186187pub fn (s &Store) check_save_files() ![]ObjectProblem {188 mut problems := []ObjectProblem{}189 mut stmt := s.db.prepare('SELECT DISTINCT f.save_id, f.hash FROM save_files f190 LEFT JOIN objects o ON o.hash = f.hash WHERE o.hash IS NULL')!191 defer {192 stmt.finalize()193 }194 for stmt.step()! {195 problems << ObjectProblem{stmt.text(1), 'save ${stmt.text(0)} needs missing object ${stmt.text(1)}'}196 }197 return problems198}199200201202pub fn (s &Store) repair_object(hash string, data []u8) ! {203 if hash_hex(data) != hash {204 return error('the copy of object ${hash} does not match its hash')205 }206 packed := zlib.compress(data)!207 mut stmt := s.db.prepare('INSERT INTO objects (hash, size, data) VALUES (?, ?, ?)208 ON CONFLICT(hash) DO UPDATE SET size = excluded.size, data = excluded.data')!209 defer {210 stmt.finalize()211 }212 stmt.bind_text(1, hash)!213 stmt.bind_int(2, i64(data.len))!214 stmt.bind_blob(3, packed)!215 stmt.step()!216}217218219pub fn (s &Store) paths_of_object(hash string) ![]string {220 mut stmt := s.db.prepare('SELECT DISTINCT path FROM save_files WHERE hash = ?')!221 defer {222 stmt.finalize()223 }224 stmt.bind_text(1, hash)!225 mut paths := []string{}226 for stmt.step()! {227 paths << stmt.text(0)228 }229 return paths230}