/* * 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 fileoper import ( "context" "fmt" "io" "net/http" "path" "strconv" "mstore/pkg/auxtool" "mstore/pkg/auxuuid" "mstore/pkg/descr" ) // PutFile type PutFileParams struct { ContentType string ContentSize string Filepath string Source io.ReadCloser } type PutFileResult struct{} const ( defaultContentType = "application/octet-stream" hcMimeType = "application/vnd.cncf.helm.chart.content.v1.tar+gzip" ) // TODO: checking catalog and file names conflict func (oper *Operator) PutFile(ctx context.Context, operatorID string, params *PutFileParams) (int, *PutFileResult, error) { var err error res := &PutFileResult{} if params.ContentSize == "" { code := http.StatusLengthRequired err = fmt.Errorf("Required Content-Size header is empty") return code, res, err } size, err := strconv.ParseInt(params.ContentSize, 10, 64) if err != nil { code := http.StatusLengthRequired return code, res, err } contentType := params.ContentType if contentType == "" { contentType = defaultContentType } resName := params.Filepath oper.iLock.WaitAndLock(resName) defer oper.iLock.Done(resName) // TODO: convert file path to a unified and secure state xfilepath, err := cleanFilepath(params.Filepath) if err != nil { code := http.StatusInternalServerError return code, res, err } filename := path.Base(xfilepath) collection := path.Dir(xfilepath) tmpname, size, checksum, err := oper.store.WriteTempFile(params.Source) if err != nil { code := http.StatusInternalServerError return code, res, err } var helmHash string var helmMeta string if contentType == hcMimeType { helmHash, helmMeta, err = oper.store.HelmMeta(tmpname) } descrExists, fileDescr, err := oper.mdb.GetFileByCollectionName(ctx, collection, filename) if err != nil { code := http.StatusInternalServerError return code, res, err } now := auxtool.TimeNow() if descrExists { fileDescr.Size = size fileDescr.Checksum = checksum fileDescr.UpdatedAt = now fileDescr.Type = contentType fileDescr.UpdatedBy = operatorID fileDescr.HelmMeta = helmMeta fileDescr.HelmHash = helmHash err = oper.mdb.UpdateFileByID(ctx, fileDescr.ID, fileDescr) if err != nil { code := http.StatusInternalServerError return code, res, err } } else { fileDescr = &descr.File{ ID: auxuuid.NewUUID(), Name: filename, Collection: collection, Size: size, Type: contentType, Checksum: checksum, CreatedAt: now, UpdatedAt: now, CreatedBy: operatorID, UpdatedBy: operatorID, HelmMeta: helmMeta, HelmHash: helmHash, } err = oper.mdb.InsertFile(ctx, fileDescr) if err != nil { code := http.StatusInternalServerError return code, res, err } } err = oper.store.HardlinkFile(tmpname, collection, filename) if err != nil { code := http.StatusInternalServerError return code, res, err } code := http.StatusOK return code, res, err }