139 lines
2.9 KiB
Go
139 lines
2.9 KiB
Go
|
package storeserver
|
||
|
|
||
|
import (
|
||
|
// "context"
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/fs"
|
||
|
"os"
|
||
|
"path"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/google/uuid"
|
||
|
|
||
|
filemeta "git.keganmyers.com/terribleplan/file-store/pkg/file/meta"
|
||
|
"git.keganmyers.com/terribleplan/file-store/pkg/ifs"
|
||
|
// "git.keganmyers.com/terribleplan/file-store/pkg/proto"
|
||
|
)
|
||
|
|
||
|
var _ = (StoreServer)((*LocalStoreServer)(nil))
|
||
|
|
||
|
func NewLocal(dir string) StoreServer {
|
||
|
f := ifs.DirFS(dir)
|
||
|
return &LocalStoreServer{f: f}
|
||
|
}
|
||
|
|
||
|
type LocalStoreServer struct {
|
||
|
f ifs.FS
|
||
|
}
|
||
|
|
||
|
func (s *LocalStoreServer) fileIdPath(fileId string) (string, error) {
|
||
|
if err := ValidateFileId(fileId); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
return path.Join(fileId[0:2], fileId[2:4], fileId[4:6], fileId[6:8], fileId), nil
|
||
|
}
|
||
|
|
||
|
func (s *LocalStoreServer) fileMetaPath(fileId string) (string, error) {
|
||
|
fileIdPath, err := s.fileIdPath(fileId)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
return path.Join(fileIdPath, "meta.json"), nil
|
||
|
}
|
||
|
|
||
|
func (s *LocalStoreServer) ListFiles(fn ListFilesFn) error {
|
||
|
return ifs.WalkDir(s.f, "/", func(path string, d fs.DirEntry, err error) error {
|
||
|
if d.IsDir() && len(path) > 48 {
|
||
|
return ifs.SkipDir
|
||
|
}
|
||
|
|
||
|
if !d.IsDir() && len(path) == 48 && d.Name() == "meta.json" {
|
||
|
fileId := path[strings.LastIndex(path, "/"):]
|
||
|
if _, err := uuid.Parse(fileId); err == nil {
|
||
|
fn(fileId)
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func (s *LocalStoreServer) ReadFile(fileId string) ([]byte, error) {
|
||
|
filePath, err := s.fileMetaPath(fileId)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
file, err := s.f.Open(filePath)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer file.Close()
|
||
|
meta, err := filemeta.ReadBytes(file)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return meta, nil
|
||
|
}
|
||
|
|
||
|
func (s *LocalStoreServer) WriteFile(fileId string, meta []byte) error {
|
||
|
dirPath, err := s.fileIdPath(fileId)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if err := s.f.MkdirAll(dirPath, 0755); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
metaPath, err := s.fileMetaPath(fileId)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
file, err := s.f.OpenFile(metaPath, os.O_WRONLY|os.O_EXCL|os.O_CREATE, 0644)
|
||
|
if err != nil {
|
||
|
if err == os.ErrExist {
|
||
|
existing, err := s.ReadFile(fileId)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if bytes.Equal(meta, existing) {
|
||
|
return nil
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
defer file.Close()
|
||
|
if _, err := file.Write(meta); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (s *LocalStoreServer) DeleteFile(fileId string) error {
|
||
|
fileIdPath, err := s.fileIdPath(fileId)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return s.f.RemoveAll(fileIdPath)
|
||
|
}
|
||
|
|
||
|
func (s *LocalStoreServer) WriteChunk(fileId string, chunkId uint16, data io.ReadCloser) error {
|
||
|
fileIdPath, err := s.fileIdPath(fileId)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
filePath := path.Join(fileIdPath, fmt.Sprintf("chunk.%04d", chunkId))
|
||
|
|
||
|
file, err := s.f.OpenFile(filePath, os.O_WRONLY|os.O_EXCL|os.O_CREATE, 0644)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if _, err := io.Copy(file, data); err != nil {
|
||
|
os.Remove(filePath)
|
||
|
return err
|
||
|
}
|
||
|
return nil
|
||
|
}
|