/* * Copyright 2026 Oleg Borodin * * This work is published and licensed under a Creative Commons * Attribution-NonCommercial-NoDerivatives 4.0 International License. * * Distribution of this work is permitted, but commercial use and * modifications are strictly prohibited. */ package storage import ( "bytes" "crypto/sha256" "encoding/hex" "errors" "fmt" "io" "os" "path/filepath" "mstore/app/logger" "mstore/pkg/auxuuid" ) const ( sha256prefix = "sha256:" filesubdir = "files" tmpsubdir = "tmps" ) type Storage struct { basepath string logg *logger.Logger } func NewStorage(basepath string) *Storage { res := &Storage{ basepath: basepath, } res.logg = logger.NewLoggerWithSubject("storage") return res } func (store *Storage) makeCollecionpath(collection string) string { return filepath.Join(store.basepath, filesubdir, collection) } func (store *Storage) makeFilepath(collection, filename string) string { return filepath.Join(store.basepath, filesubdir, collection, filename) } func (store *Storage) makeTmppath(tmpname string) string { return filepath.Join(store.basepath, tmpsubdir, tmpname) } func (store *Storage) makeTmpsubdir() string { return filepath.Join(store.basepath, tmpsubdir) } func (store *Storage) makeFilesubdir(collection, filename string) string { return filepath.Join(store.basepath, filesubdir) } func (store *Storage) GetFileReader(collection, filename string) (io.ReadCloser, error) { var err error var res io.ReadCloser filename = store.makeFilepath(collection, filename) file, err := os.OpenFile(filename, os.O_RDONLY, 0) if err != nil { return res, err } res = file return res, err } func (store *Storage) WriteTempFile(source io.Reader) (string, int64, string, error) { var err error var size int64 var csum string tmpname := auxuuid.NewUUID() tmpname = fmt.Sprintf("file-%s.tmp", tmpname) tmppath := store.makeTmppath(tmpname) tmpdirpath := store.makeTmpsubdir() err = os.MkdirAll(tmpdirpath, 0750) if err != nil { return tmpname, size, csum, err } file, err := os.OpenFile(tmppath, os.O_WRONLY|os.O_CREATE, 0640) if err != nil { return tmpname, size, csum, err } defer file.Close() hasher := sha256.New() writer := io.MultiWriter(file, hasher) size, err = io.Copy(writer, source) if err != nil { return tmpname, size, csum, err } csum = hex.EncodeToString(hasher.Sum(nil)) csum = sha256prefix + csum return tmpname, size, csum, err } func (store *Storage) HardlinkFile(tmpname, collection, filename string) error { var err error dirname := store.makeCollecionpath(collection) _, err = os.Stat(dirname) if errors.Is(err, os.ErrNotExist) { err = os.MkdirAll(dirname, 0750) if err != nil { return err } } if err != nil { return err } filename = store.makeFilepath(collection, filename) os.Remove(filename) // TODO tmpname = store.makeTmppath(tmpname) err = os.Link(tmpname, filename) if err != nil { return err } err = os.Remove(tmpname) if err != nil { return err } return err } func (store *Storage) DeleteFile(collection, filename string) error { var err error filename = store.makeFilepath(collection, filename) err = os.Remove(filename) if err != nil { return err } // TODO: safe dir removing dirname := store.makeCollecionpath(collection) err = os.RemoveAll(dirname) if err != nil { return err } return err } const ( upsubdir = "uploads" blobsubdir = "blobs" ) func (store *Storage) makeUppath(upname string) string { return filepath.Join(store.basepath, upsubdir, upname) + ".bin" } func (store *Storage) makeUpsubdir() string { return filepath.Join(store.basepath, upsubdir) } func (store *Storage) makeBlobpath(upname string) string { return filepath.Join(store.basepath, blobsubdir, upname) + ".bin" } func (store *Storage) makeBlobsubdir() string { return filepath.Join(store.basepath, blobsubdir) } func (store *Storage) WriteUpload(uploadID string, source io.Reader) (int64, string, error) { var err error var recsize int64 var recsum string uploadDir := store.makeUpsubdir() _, err = os.Stat(uploadDir) if errors.Is(err, os.ErrNotExist) { err = os.MkdirAll(uploadDir, 0750) if err != nil { return recsize, recsum, err } } if err != nil { return recsize, recsum, err } uploadPath := store.makeUppath(uploadID) uploadFile, err := os.OpenFile(uploadPath, os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return recsize, recsum, err } defer uploadFile.Close() hasher := sha256.New() // TODO: upload cheking streamWriter := io.MultiWriter(uploadFile, hasher) recsize, err = io.Copy(streamWriter, source) if err != nil { return recsize, recsum, err } recsum = hex.EncodeToString(hasher.Sum(nil)) recsum = sha256prefix + recsum return recsize, recsum, err } func (store *Storage) LinkUpload(reference, digest string) error { var err error uploadPath := store.makeUppath(reference) blobdir := store.makeBlobsubdir() _, err = os.Stat(blobdir) if errors.Is(err, os.ErrNotExist) { err = os.MkdirAll(blobdir, 0750) if err != nil { return err } } if err != nil { return err } blobPath := store.makeBlobpath(digest) _, err = os.Stat(blobPath) if err == nil { err = os.Remove(blobPath) if err != nil { return err } } if errors.Is(err, os.ErrNotExist) { err = nil } if err != nil { return err } err = os.Link(uploadPath, blobPath) if err != nil { return err } err = os.Remove(uploadPath) if err != nil { return err } return err } func (store *Storage) RemoveUpload(digest string) error { var err error uploadPath := store.makeUppath(digest) err = os.Remove(uploadPath) if err != nil { return err } return err } func (st *Storage) UploadExists(name, reference string) (bool, int64, error) { var err error var fileSize int64 uploadPath := st.makeUppath(reference) fileStat, err := os.Stat(uploadPath) if errors.Is(err, os.ErrNotExist) { return false, 0, nil } if err != nil { return false, 0, err } fileSize = fileStat.Size() return true, fileSize, err } func (store *Storage) WriteBlob(digest string, source io.Reader) (int64, string, error) { var err error var recsize int64 var recsum string //defer source.Close() blobdir := store.makeBlobsubdir() _, err = os.Stat(blobdir) if errors.Is(err, os.ErrNotExist) { err = os.MkdirAll(blobdir, 0750) if err != nil { return recsize, digest, err } } if err != nil { return recsize, digest, err } blobpath := store.makeBlobpath(digest) blobfile, err := os.OpenFile(blobpath, os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return recsize, digest, err } defer blobfile.Close() hasher := sha256.New() // TODO multiWriter := io.MultiWriter(blobfile, hasher) recsize, err = io.Copy(multiWriter, source) if err != nil { return recsize, digest, err } recsum = hex.EncodeToString(hasher.Sum(nil)) recsum = sha256prefix + recsum return recsize, recsum, err } func (st *Storage) BlobExists(digest string) (bool, int64, error) { var err error var fileSize int64 blobPath := st.makeBlobpath(digest) fileStat, err := os.Stat(blobPath) if errors.Is(err, os.ErrNotExist) { return false, 0, nil } if err != nil { return false, 0, err } fileSize = fileStat.Size() return true, fileSize, err } func (store *Storage) BlobReader(digest string) (int64, io.ReadCloser, error) { var err error var filesize int64 blobpath := store.makeBlobpath(digest) emptyReadCloser := io.NopCloser(bytes.NewReader(nil)) file, err := os.OpenFile(blobpath, os.O_RDONLY, 0) if err != nil { return filesize, emptyReadCloser, err } defer func() { if err != nil { file.Close() } }() filestat, err := file.Stat() if err != nil { return filesize, emptyReadCloser, err } filesize = filestat.Size() return filesize, file, err } func (store *Storage) DeleteBlob(digest string) error { var err error blobpath := store.makeBlobpath(digest) err = os.Remove(blobpath) if errors.Is(err, os.ErrNotExist) { err = nil } if err != nil { return err } return err }