| 192 | } |
| 193 | |
| 194 | func (l *Local) Put(ctx context.Context, key string, body io.Reader, meta PutMeta) (Entry, error) { |
| 195 | if err := ctx.Err(); err != nil { |
| 196 | return Entry{}, err |
| 197 | } |
| 198 | full, err := l.resolve(key) |
| 199 | if err != nil { |
| 200 | return Entry{}, err |
| 201 | } |
| 202 | if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { |
| 203 | return Entry{}, fmt.Errorf("fstable/local: mkdir: %w", err) |
| 204 | } |
| 205 | // Write to a sibling temp file then rename for atomicity. |
| 206 | tmp, err := os.CreateTemp(filepath.Dir(full), ".gj-fstable-*") |
| 207 | if err != nil { |
| 208 | return Entry{}, fmt.Errorf("fstable/local: temp: %w", err) |
| 209 | } |
| 210 | tmpName := tmp.Name() |
| 211 | n, copyErr := io.Copy(tmp, body) |
| 212 | closeErr := tmp.Close() |
| 213 | if copyErr != nil { |
| 214 | _ = os.Remove(tmpName) |
| 215 | return Entry{}, fmt.Errorf("fstable/local: write: %w", copyErr) |
| 216 | } |
| 217 | if closeErr != nil { |
| 218 | _ = os.Remove(tmpName) |
| 219 | return Entry{}, fmt.Errorf("fstable/local: close temp: %w", closeErr) |
| 220 | } |
| 221 | if err := os.Rename(tmpName, full); err != nil { |
| 222 | _ = os.Remove(tmpName) |
| 223 | return Entry{}, fmt.Errorf("fstable/local: rename: %w", err) |
| 224 | } |
| 225 | info, err := os.Stat(full) |
| 226 | if err != nil { |
| 227 | return Entry{}, err |
| 228 | } |
| 229 | ct := meta.ContentType |
| 230 | if ct == "" { |
| 231 | ct = guessContentType(full) |
| 232 | } |
| 233 | return Entry{ |
| 234 | Key: key, |
| 235 | Size: n, |
| 236 | ContentType: ct, |
| 237 | ETag: fmt.Sprintf("%d-%d", info.Size(), info.ModTime().UnixNano()), |
| 238 | ModifiedAt: info.ModTime(), |
| 239 | }, nil |
| 240 | } |
| 241 | |
| 242 | func (l *Local) Delete(ctx context.Context, key string) error { |
| 243 | if err := ctx.Err(); err != nil { |