241 lines
5.0 KiB
Go
241 lines
5.0 KiB
Go
package storage
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type FSStore struct {
|
|
root string
|
|
objects string
|
|
}
|
|
|
|
func NewFS(dir string) (*FSStore, error) {
|
|
if dir == "" {
|
|
return nil, errors.New("empty storage dir")
|
|
}
|
|
o := filepath.Join(dir, "objects")
|
|
if err := os.MkdirAll(o, 0o755); err != nil {
|
|
return nil, err
|
|
}
|
|
return &FSStore{root: dir, objects: o}, nil
|
|
}
|
|
|
|
func (s *FSStore) pathFlat(hash string) (string, error) {
|
|
if hash == "" {
|
|
return "", errors.New("empty hash")
|
|
}
|
|
return filepath.Join(s.objects, hash), nil
|
|
}
|
|
|
|
func isHexHash(name string) bool {
|
|
if len(name) != 64 {
|
|
return false
|
|
}
|
|
for i := 0; i < 64; i++ {
|
|
c := name[i]
|
|
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *FSStore) findBlobPath(hash string) (string, error) {
|
|
if hash == "" {
|
|
return "", errors.New("empty hash")
|
|
}
|
|
// 1) flat
|
|
if p, _ := s.pathFlat(hash); fileExists(p) {
|
|
return p, nil
|
|
}
|
|
// 2) objects/<hash>/{blob,data,content}
|
|
dir := filepath.Join(s.objects, hash)
|
|
for _, cand := range []string{"blob", "data", "content"} {
|
|
p := filepath.Join(dir, cand)
|
|
if fileExists(p) {
|
|
return p, nil
|
|
}
|
|
}
|
|
// 3) objects/<hash>/<single file>
|
|
if st, err := os.Stat(dir); err == nil && st.IsDir() {
|
|
ents, _ := os.ReadDir(dir)
|
|
var picked string
|
|
var pickedMod time.Time
|
|
for _, de := range ents {
|
|
if de.IsDir() {
|
|
continue
|
|
}
|
|
p := filepath.Join(dir, de.Name())
|
|
fi, err := os.Stat(p)
|
|
if err == nil && fi.Mode().IsRegular() {
|
|
if picked == "" || fi.ModTime().After(pickedMod) {
|
|
picked, pickedMod = p, fi.ModTime()
|
|
}
|
|
}
|
|
}
|
|
if picked != "" {
|
|
return picked, nil
|
|
}
|
|
}
|
|
// 4) two-level prefix objects/aa/<hash>
|
|
if len(hash) >= 2 {
|
|
p := filepath.Join(s.objects, hash[:2], hash)
|
|
if fileExists(p) {
|
|
return p, nil
|
|
}
|
|
}
|
|
// 5) recursive search
|
|
var best string
|
|
var bestMod time.Time
|
|
_ = filepath.WalkDir(s.objects, func(p string, d fs.DirEntry, err error) error {
|
|
if err != nil || d.IsDir() {
|
|
return nil
|
|
}
|
|
base := filepath.Base(p)
|
|
if base == hash {
|
|
best = p
|
|
return fs.SkipDir
|
|
}
|
|
parent := filepath.Base(filepath.Dir(p))
|
|
if parent == hash {
|
|
if fi, err := os.Stat(p); err == nil && fi.Mode().IsRegular() {
|
|
if best == "" || fi.ModTime().After(bestMod) {
|
|
best, bestMod = p, fi.ModTime()
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if best != "" {
|
|
return best, nil
|
|
}
|
|
return "", os.ErrNotExist
|
|
}
|
|
|
|
func fileExists(p string) bool {
|
|
fi, err := os.Stat(p)
|
|
return err == nil && fi.Mode().IsRegular()
|
|
}
|
|
|
|
func (s *FSStore) Put(hash string, r io.Reader) error {
|
|
p, err := s.pathFlat(hash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
return err
|
|
}
|
|
tmp := p + ".tmp"
|
|
f, err := os.Create(tmp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, werr := io.Copy(f, r)
|
|
cerr := f.Close()
|
|
if werr != nil {
|
|
_ = os.Remove(tmp)
|
|
return werr
|
|
}
|
|
if cerr != nil {
|
|
_ = os.Remove(tmp)
|
|
return cerr
|
|
}
|
|
return os.Rename(tmp, p)
|
|
}
|
|
|
|
func (s *FSStore) Get(hash string) (io.ReadCloser, int64, error) {
|
|
p, err := s.findBlobPath(hash)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
f, err := os.Open(p)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
st, err := f.Stat()
|
|
if err != nil {
|
|
return f, 0, nil
|
|
}
|
|
return f, st.Size(), nil
|
|
}
|
|
|
|
func (s *FSStore) Delete(hash string) error {
|
|
if p, _ := s.pathFlat(hash); fileExists(p) {
|
|
if err := os.Remove(p); err == nil || errors.Is(err, os.ErrNotExist) {
|
|
return nil
|
|
}
|
|
}
|
|
dir := filepath.Join(s.objects, hash)
|
|
for _, cand := range []string{"blob", "data", "content"} {
|
|
p := filepath.Join(dir, cand)
|
|
if fileExists(p) {
|
|
if err := os.Remove(p); err == nil || errors.Is(err, os.ErrNotExist) {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
if len(hash) >= 2 {
|
|
p := filepath.Join(s.objects, hash[:2], hash)
|
|
if fileExists(p) {
|
|
if err := os.Remove(p); err == nil || errors.Is(err, os.ErrNotExist) {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
if p, err := s.findBlobPath(hash); err == nil {
|
|
if err := os.Remove(p); err == nil || errors.Is(err, os.ErrNotExist) {
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *FSStore) Walk(fn func(hash string, size int64, mod time.Time) error) error {
|
|
type rec struct {
|
|
size int64
|
|
mod time.Time
|
|
}
|
|
agg := make(map[string]rec)
|
|
_ = filepath.WalkDir(s.objects, func(p string, d fs.DirEntry, err error) error {
|
|
if err != nil || d.IsDir() {
|
|
return nil
|
|
}
|
|
fi, err := os.Stat(p)
|
|
if err != nil || !fi.Mode().IsRegular() {
|
|
return nil
|
|
}
|
|
base := filepath.Base(p)
|
|
if isHexHash(base) {
|
|
if r, ok := agg[base]; !ok || fi.ModTime().After(r.mod) {
|
|
agg[base] = rec{fi.Size(), fi.ModTime()}
|
|
}
|
|
return nil
|
|
}
|
|
parent := filepath.Base(filepath.Dir(p))
|
|
if isHexHash(parent) {
|
|
if r, ok := agg[parent]; !ok || fi.ModTime().After(r.mod) {
|
|
agg[parent] = rec{fi.Size(), fi.ModTime()}
|
|
}
|
|
return nil
|
|
}
|
|
if len(base) == 64 && isHexHash(strings.ToLower(base)) {
|
|
if r, ok := agg[base]; !ok || fi.ModTime().After(r.mod) {
|
|
agg[base] = rec{fi.Size(), fi.ModTime()}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
for h, r := range agg {
|
|
if err := fn(h, r.size, r.mod); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|