diff --git a/html/src/Components/ShareComponent.tsx b/html/src/Components/ShareComponent.tsx index 2b66e5a..69413de 100644 --- a/html/src/Components/ShareComponent.tsx +++ b/html/src/Components/ShareComponent.tsx @@ -18,7 +18,7 @@ export function ShareComponent(props: {share: Share}) { const name = share.name const count = share.count const size = share.size - const countString = count?(count + ' items' + (count > 1 ? 's' : '')):"empty" + const countString = count?(count + ' item' + (count > 1 ? 's' : '')):"empty" // Function const deleteShare = () => { diff --git a/hupload/pkg/apiws/storageservice/storageservice.go b/hupload/pkg/apiws/storageservice/storageservice.go index d0665c8..8845189 100644 --- a/hupload/pkg/apiws/storageservice/storageservice.go +++ b/hupload/pkg/apiws/storageservice/storageservice.go @@ -6,9 +6,11 @@ import ( ) type Share struct { - Name string `json:"name"` - Created time.Time `json:"created"` - Owner string `json:"owner"` + Name string `json:"name"` + DateCreated time.Time `json:"created"` + Owner string `json:"owner"` + Size int64 `json:"size"` + Count int64 `json:"count"` } type Item struct { @@ -16,7 +18,8 @@ type Item struct { ItemInfo ItemInfo } type ItemInfo struct { - Size int64 + Size int64 + DateModified time.Time } // BackendInterface must be implemented by any backend @@ -27,6 +30,9 @@ type StorageServiceInterface interface { // CreateItem creates a new item in a share CreateItem(string, string, *bufio.Reader) (*Item, error) + // GetShare returns the share identified by share + GetShare(string) (*Share, error) + // ListShares returns the list of shares available ListShares() ([]Share, error) @@ -41,4 +47,7 @@ type StorageServiceInterface interface { // GetItem returns the item identified by share and item GetItemData(string, string) (*bufio.Reader, error) + + // UpdateMetadata updates the metadata of share + UpdateMetadata(string) error } diff --git a/hupload/pkg/apiws/storageservice/storageservicefile.go b/hupload/pkg/apiws/storageservice/storageservicefile.go index f7679d7..6e469cf 100644 --- a/hupload/pkg/apiws/storageservice/storageservicefile.go +++ b/hupload/pkg/apiws/storageservice/storageservicefile.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "io" + "log/slog" "os" "path" "sort" @@ -32,7 +33,7 @@ func NewFileBackend(m map[string]any) *FileBackend { // initialize creates the root directory for the backend func (b *FileBackend) initialize() { - path := b.Options["path"].(string) + path := b.stringOption("path") if path == "" { panic("path is required") } @@ -42,23 +43,39 @@ func (b *FileBackend) initialize() { } } +func (b *FileBackend) stringOption(o string) string { + v, ok := b.Options[o].(string) + if !ok { + return "" + } + return v +} + +func (b *FileBackend) int64Option(o string) int64 { + v, ok := b.Options[o].(int) + if !ok { + return 0 + } + return int64(v) +} + func (b *FileBackend) CreateShare(s string, o string) error { - _, err := os.Stat(path.Join(b.Options["path"].(string), s)) + _, err := os.Stat(path.Join(b.stringOption("path"), s)) if err == nil { return errors.New("share already exists") } - err = os.Mkdir(path.Join(b.Options["path"].(string), s), 0755) + err = os.Mkdir(path.Join(b.stringOption("path"), s), 0755) if err != nil { return errors.New("cannot create share") } m := Share{ - Name: s, - Owner: o, - Created: time.Now(), + Name: s, + Owner: o, + DateCreated: time.Now(), } - f, err := os.Create(path.Join(b.Options["path"].(string), s, ".metadata")) + f, err := os.Create(path.Join(b.stringOption("path"), s, ".metadata")) if err != nil { return err } @@ -71,17 +88,42 @@ func (b *FileBackend) CreateShare(s string, o string) error { } func (b *FileBackend) CreateItem(s string, i string, r *bufio.Reader) (*Item, error) { - p := path.Join(b.Options["path"].(string), s, i) + p := path.Join(b.stringOption("path"), s, i) f, err := os.Create(p + suffix) if err != nil { return nil, errors.New("cannot create item") } defer f.Close() - _, err = io.Copy(f, r) - if err != nil { - os.Remove(p + suffix) - return nil, errors.New("cannot copy item content") + max := b.int64Option("max_bytes") + if max > 0 { + // Check if there is a max bytes associated to share + share, err := b.GetShare(s) + if err != nil { + return nil, errors.New("cannot get share") + } + maxWrite := max - share.Size + if maxWrite <= 0 { + return nil, errors.New("Max share capacity already reached") + } + reader := io.LimitReader(r, maxWrite) + written, err := io.Copy(f, reader) + if err != nil { + os.Remove(p + suffix) + return nil, errors.New("cannot copy item content") + } + + if written == 0 || written == maxWrite { + os.Remove(p + suffix) + return nil, errors.New("Max share capacity reached") + } + + } else { + _, err := io.Copy(f, r) + if err != nil { + os.Remove(p + suffix) + return nil, errors.New("cannot copy item content") + } } err = os.Rename(p+suffix, p) if err != nil { @@ -95,36 +137,47 @@ func (b *FileBackend) CreateItem(s string, i string, r *bufio.Reader) (*Item, er return item, nil } +func (b *FileBackend) GetShare(s string) (*Share, error) { + fm, err := os.Open(path.Join(b.stringOption("path"), s, ".metadata")) + if err != nil { + return nil, err + } + defer fm.Close() + + m := Share{} + err = json.NewDecoder(fm).Decode(&m) + if err != nil { + return nil, err + } + return &m, nil +} + func (b *FileBackend) ListShares() ([]Share, error) { - d, err := os.ReadDir(b.Options["path"].(string)) + d, err := os.ReadDir(b.stringOption("path")) if err != nil { return nil, err } r := []Share{} + + // Shares loop for _, f := range d { if f.IsDir() { - fm, err := os.Open(path.Join(b.Options["path"].(string), f.Name(), ".metadata")) + m, err := b.GetShare(f.Name()) if err != nil { continue } - defer fm.Close() - m := Share{} - err = json.NewDecoder(fm).Decode(&m) - if err != nil { - return nil, err - } - r = append(r, m) + r = append(r, *m) } } sort.Slice(r, func(i, j int) bool { - return r[i].Created.After(r[j].Created) + return r[i].DateCreated.After(r[j].DateCreated) }) return r, nil } func (b *FileBackend) DeleteShare(s string) error { - sharePath := path.Join(b.Options["path"].(string), s) + sharePath := path.Join(b.stringOption("path"), s) err := os.RemoveAll(sharePath) if err != nil { return err @@ -133,7 +186,7 @@ func (b *FileBackend) DeleteShare(s string) error { } func (b *FileBackend) ListShare(s string) ([]Item, error) { - d, err := os.ReadDir(path.Join(b.Options["path"].(string), s)) + d, err := os.ReadDir(path.Join(b.stringOption("path"), s)) if err != nil { return nil, err } @@ -148,26 +201,79 @@ func (b *FileBackend) ListShare(s string) ([]Item, error) { } r = append(r, *i) } + + sort.Slice(r, func(i, j int) bool { + return r[i].ItemInfo.DateModified.After(r[j].ItemInfo.DateModified) + }) + return r, nil } func (b *FileBackend) GetItem(s string, i string) (*Item, error) { - p := path.Join(b.Options["path"].(string), s, i) + p := path.Join(b.stringOption("path"), s, i) stat, err := os.Stat(p) if err != nil { return nil, err } return &Item{ Path: path.Join(s, i), - ItemInfo: ItemInfo{Size: stat.Size()}, + ItemInfo: ItemInfo{Size: stat.Size(), DateModified: stat.ModTime()}, }, nil } func (b *FileBackend) GetItemData(s string, i string) (*bufio.Reader, error) { - p := path.Join(b.Options["path"].(string), s, i) + p := path.Join(b.stringOption("path"), s, i) f, err := os.Open(p) if err != nil { return nil, err } return bufio.NewReader(f), nil } + +func (b *FileBackend) UpdateMetadata(s string) error { + sd, err := os.ReadDir(path.Join(b.stringOption("path"), s)) + if err != nil { + return err + } + + fm, err := os.OpenFile(path.Join(b.stringOption("path"), s, ".metadata"), os.O_RDWR, 0644) + if err != nil { + return err + } + defer fm.Close() + + m := Share{} + err = json.NewDecoder(fm).Decode(&m) + if err != nil { + return err + } + + m.Size = 0 + m.Count = 0 + + // Share content loop + for _, i := range sd { + if strings.HasPrefix(i.Name(), ".") { + continue + } + info, err := i.Info() + if err != nil { + slog.Error("cannot get file info", slog.String("error", err.Error())) + continue + } + m.Size += info.Size() + m.Count += 1 + } + + _, err = fm.Seek(0, 0) + if err != nil { + return err + } + + err = json.NewEncoder(fm).Encode(m) + if err != nil { + return err + } + + return nil +} diff --git a/hupload/routes.go b/hupload/routes.go index b2af262..a37ab20 100644 --- a/hupload/routes.go +++ b/hupload/routes.go @@ -58,6 +58,7 @@ func postItem(w http.ResponseWriter, r *http.Request) { b := bufio.NewReader(np) item, err := api.StorageService.CreateItem(r.PathValue("share"), np.FileName(), b) if err != nil { + w.WriteHeader(http.StatusNotAcceptable) _, _ = w.Write([]byte(err.Error())) return } @@ -68,6 +69,12 @@ func postItem(w http.ResponseWriter, r *http.Request) { return } + err = api.StorageService.UpdateMetadata(r.PathValue("share")) + if err != nil { + _, _ = w.Write([]byte(err.Error())) + return + } + _, _ = w.Write(c) }