mirror of
https://github.com/AlistGo/alist.git
synced 2025-12-19 11:00:06 +08:00
Adds storage class information to file metadata and API responses. This change introduces the ability to store file storage classes in file metadata and display them in API responses. This allows users to view a file's storage tier (e.g., S3 Standard, Glacier), enhancing data management capabilities. Implementation details include: - Introducing the StorageClassProvider interface and the ObjWrapStorageClass structure to uniformly handle and communicate object storage class information. - Updated file metadata structures (e.g., ArchiveObj, FileInfo, RespFile) to include a StorageClass field. - Modified relevant API response functions (e.g., GetFileInfo, GetFileList) to populate and return storage classes. - Integrated functionality for retrieving object storage classes from underlying storage systems (e.g., S3) and wrapping them in lists.
431 lines
12 KiB
Go
431 lines
12 KiB
Go
package handles
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/alist-org/alist/v3/internal/task"
|
|
"net/url"
|
|
stdpath "path"
|
|
|
|
"github.com/alist-org/alist/v3/internal/archive/tool"
|
|
"github.com/alist-org/alist/v3/internal/conf"
|
|
"github.com/alist-org/alist/v3/internal/errs"
|
|
"github.com/alist-org/alist/v3/internal/fs"
|
|
"github.com/alist-org/alist/v3/internal/model"
|
|
"github.com/alist-org/alist/v3/internal/op"
|
|
"github.com/alist-org/alist/v3/internal/setting"
|
|
"github.com/alist-org/alist/v3/internal/sign"
|
|
"github.com/alist-org/alist/v3/pkg/utils"
|
|
"github.com/alist-org/alist/v3/server/common"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/pkg/errors"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type ArchiveMetaReq struct {
|
|
Path string `json:"path" form:"path"`
|
|
Password string `json:"password" form:"password"`
|
|
Refresh bool `json:"refresh" form:"refresh"`
|
|
ArchivePass string `json:"archive_pass" form:"archive_pass"`
|
|
}
|
|
|
|
type ArchiveMetaResp struct {
|
|
Comment string `json:"comment"`
|
|
IsEncrypted bool `json:"encrypted"`
|
|
Content []ArchiveContentResp `json:"content"`
|
|
Sort *model.Sort `json:"sort,omitempty"`
|
|
RawURL string `json:"raw_url"`
|
|
Sign string `json:"sign"`
|
|
}
|
|
|
|
type ArchiveContentResp struct {
|
|
ObjResp
|
|
Children []ArchiveContentResp `json:"children"`
|
|
}
|
|
|
|
func toObjsRespWithoutSignAndThumb(obj model.Obj) ObjResp {
|
|
storageClass, _ := model.GetStorageClass(obj)
|
|
return ObjResp{
|
|
Name: obj.GetName(),
|
|
Size: obj.GetSize(),
|
|
IsDir: obj.IsDir(),
|
|
Modified: obj.ModTime(),
|
|
Created: obj.CreateTime(),
|
|
HashInfoStr: obj.GetHash().String(),
|
|
HashInfo: obj.GetHash().Export(),
|
|
Sign: "",
|
|
Thumb: "",
|
|
Type: utils.GetObjType(obj.GetName(), obj.IsDir()),
|
|
StorageClass: storageClass,
|
|
}
|
|
}
|
|
|
|
func toContentResp(objs []model.ObjTree) []ArchiveContentResp {
|
|
if objs == nil {
|
|
return nil
|
|
}
|
|
ret, _ := utils.SliceConvert(objs, func(src model.ObjTree) (ArchiveContentResp, error) {
|
|
return ArchiveContentResp{
|
|
ObjResp: toObjsRespWithoutSignAndThumb(src),
|
|
Children: toContentResp(src.GetChildren()),
|
|
}, nil
|
|
})
|
|
return ret
|
|
}
|
|
|
|
func FsArchiveMeta(c *gin.Context) {
|
|
var req ArchiveMetaReq
|
|
if err := c.ShouldBind(&req); err != nil {
|
|
common.ErrorResp(c, err, 400)
|
|
return
|
|
}
|
|
user := c.MustGet("user").(*model.User)
|
|
reqPath, err := user.JoinPath(req.Path)
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 403)
|
|
return
|
|
}
|
|
if !common.CheckPathLimitWithRoles(user, reqPath) {
|
|
common.ErrorResp(c, errs.PermissionDenied, 403)
|
|
return
|
|
}
|
|
perm := common.MergeRolePermissions(user, reqPath)
|
|
if !common.HasPermission(perm, common.PermReadArchives) {
|
|
common.ErrorResp(c, errs.PermissionDenied, 403)
|
|
return
|
|
}
|
|
meta, err := op.GetNearestMeta(reqPath)
|
|
if err != nil {
|
|
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
|
common.ErrorResp(c, err, 500, true)
|
|
return
|
|
}
|
|
}
|
|
c.Set("meta", meta)
|
|
if !common.CanAccess(user, meta, reqPath, req.Password) {
|
|
common.ErrorStrResp(c, "password is incorrect or you have no permission", 403)
|
|
return
|
|
}
|
|
archiveArgs := model.ArchiveArgs{
|
|
LinkArgs: model.LinkArgs{
|
|
Header: c.Request.Header,
|
|
Type: c.Query("type"),
|
|
HttpReq: c.Request,
|
|
},
|
|
Password: req.ArchivePass,
|
|
}
|
|
ret, err := fs.ArchiveMeta(c, reqPath, model.ArchiveMetaArgs{
|
|
ArchiveArgs: archiveArgs,
|
|
Refresh: req.Refresh,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, errs.WrongArchivePassword) {
|
|
common.ErrorResp(c, err, 202)
|
|
} else {
|
|
common.ErrorResp(c, err, 500)
|
|
}
|
|
return
|
|
}
|
|
s := ""
|
|
if isEncrypt(meta, reqPath) || setting.GetBool(conf.SignAll) {
|
|
s = sign.SignArchive(reqPath)
|
|
}
|
|
api := "/ae"
|
|
if ret.DriverProviding {
|
|
api = "/ad"
|
|
}
|
|
common.SuccessResp(c, ArchiveMetaResp{
|
|
Comment: ret.GetComment(),
|
|
IsEncrypted: ret.IsEncrypted(),
|
|
Content: toContentResp(ret.GetTree()),
|
|
Sort: ret.Sort,
|
|
RawURL: fmt.Sprintf("%s%s%s", common.GetApiUrl(c.Request), api, utils.EncodePath(reqPath, true)),
|
|
Sign: s,
|
|
})
|
|
}
|
|
|
|
type ArchiveListReq struct {
|
|
ArchiveMetaReq
|
|
model.PageReq
|
|
InnerPath string `json:"inner_path" form:"inner_path"`
|
|
}
|
|
|
|
type ArchiveListResp struct {
|
|
Content []ObjResp `json:"content"`
|
|
Total int64 `json:"total"`
|
|
}
|
|
|
|
func FsArchiveList(c *gin.Context) {
|
|
var req ArchiveListReq
|
|
if err := c.ShouldBind(&req); err != nil {
|
|
common.ErrorResp(c, err, 400)
|
|
return
|
|
}
|
|
req.Validate()
|
|
user := c.MustGet("user").(*model.User)
|
|
reqPath, err := user.JoinPath(req.Path)
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 403)
|
|
return
|
|
}
|
|
if !common.CheckPathLimitWithRoles(user, reqPath) {
|
|
common.ErrorResp(c, errs.PermissionDenied, 403)
|
|
return
|
|
}
|
|
perm := common.MergeRolePermissions(user, reqPath)
|
|
if !common.HasPermission(perm, common.PermReadArchives) {
|
|
common.ErrorResp(c, errs.PermissionDenied, 403)
|
|
return
|
|
}
|
|
meta, err := op.GetNearestMeta(reqPath)
|
|
if err != nil {
|
|
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
|
common.ErrorResp(c, err, 500, true)
|
|
return
|
|
}
|
|
}
|
|
c.Set("meta", meta)
|
|
if !common.CanAccess(user, meta, reqPath, req.Password) {
|
|
common.ErrorStrResp(c, "password is incorrect or you have no permission", 403)
|
|
return
|
|
}
|
|
objs, err := fs.ArchiveList(c, reqPath, model.ArchiveListArgs{
|
|
ArchiveInnerArgs: model.ArchiveInnerArgs{
|
|
ArchiveArgs: model.ArchiveArgs{
|
|
LinkArgs: model.LinkArgs{
|
|
Header: c.Request.Header,
|
|
Type: c.Query("type"),
|
|
HttpReq: c.Request,
|
|
},
|
|
Password: req.ArchivePass,
|
|
},
|
|
InnerPath: utils.FixAndCleanPath(req.InnerPath),
|
|
},
|
|
Refresh: req.Refresh,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, errs.WrongArchivePassword) {
|
|
common.ErrorResp(c, err, 202)
|
|
} else {
|
|
common.ErrorResp(c, err, 500)
|
|
}
|
|
return
|
|
}
|
|
total, objs := pagination(objs, &req.PageReq)
|
|
ret, _ := utils.SliceConvert(objs, func(src model.Obj) (ObjResp, error) {
|
|
return toObjsRespWithoutSignAndThumb(src), nil
|
|
})
|
|
common.SuccessResp(c, ArchiveListResp{
|
|
Content: ret,
|
|
Total: int64(total),
|
|
})
|
|
}
|
|
|
|
type StringOrArray []string
|
|
|
|
func (s *StringOrArray) UnmarshalJSON(data []byte) error {
|
|
var value string
|
|
if err := json.Unmarshal(data, &value); err == nil {
|
|
*s = []string{value}
|
|
return nil
|
|
}
|
|
var sliceValue []string
|
|
if err := json.Unmarshal(data, &sliceValue); err != nil {
|
|
return err
|
|
}
|
|
*s = sliceValue
|
|
return nil
|
|
}
|
|
|
|
type ArchiveDecompressReq struct {
|
|
SrcDir string `json:"src_dir" form:"src_dir"`
|
|
DstDir string `json:"dst_dir" form:"dst_dir"`
|
|
Name StringOrArray `json:"name" form:"name"`
|
|
ArchivePass string `json:"archive_pass" form:"archive_pass"`
|
|
InnerPath string `json:"inner_path" form:"inner_path"`
|
|
CacheFull bool `json:"cache_full" form:"cache_full"`
|
|
PutIntoNewDir bool `json:"put_into_new_dir" form:"put_into_new_dir"`
|
|
}
|
|
|
|
func FsArchiveDecompress(c *gin.Context) {
|
|
var req ArchiveDecompressReq
|
|
if err := c.ShouldBind(&req); err != nil {
|
|
common.ErrorResp(c, err, 400)
|
|
return
|
|
}
|
|
user := c.MustGet("user").(*model.User)
|
|
srcPaths := make([]string, 0, len(req.Name))
|
|
for _, name := range req.Name {
|
|
srcPath, err := user.JoinPath(stdpath.Join(req.SrcDir, name))
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 403)
|
|
return
|
|
}
|
|
if !common.CheckPathLimitWithRoles(user, srcPath) {
|
|
common.ErrorResp(c, errs.PermissionDenied, 403)
|
|
return
|
|
}
|
|
srcPaths = append(srcPaths, srcPath)
|
|
}
|
|
dstDir, err := user.JoinPath(req.DstDir)
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 403)
|
|
return
|
|
}
|
|
if !common.CheckPathLimitWithRoles(user, dstDir) {
|
|
common.ErrorResp(c, errs.PermissionDenied, 403)
|
|
return
|
|
}
|
|
tasks := make([]task.TaskExtensionInfo, 0, len(srcPaths))
|
|
for _, srcPath := range srcPaths {
|
|
perm := common.MergeRolePermissions(user, srcPath)
|
|
if !common.HasPermission(perm, common.PermDecompress) {
|
|
common.ErrorResp(c, errs.PermissionDenied, 403)
|
|
return
|
|
}
|
|
t, e := fs.ArchiveDecompress(c, srcPath, dstDir, model.ArchiveDecompressArgs{
|
|
ArchiveInnerArgs: model.ArchiveInnerArgs{
|
|
ArchiveArgs: model.ArchiveArgs{
|
|
LinkArgs: model.LinkArgs{
|
|
Header: c.Request.Header,
|
|
Type: c.Query("type"),
|
|
HttpReq: c.Request,
|
|
},
|
|
Password: req.ArchivePass,
|
|
},
|
|
InnerPath: utils.FixAndCleanPath(req.InnerPath),
|
|
},
|
|
CacheFull: req.CacheFull,
|
|
PutIntoNewDir: req.PutIntoNewDir,
|
|
})
|
|
if e != nil {
|
|
if errors.Is(e, errs.WrongArchivePassword) {
|
|
common.ErrorResp(c, e, 202)
|
|
} else {
|
|
common.ErrorResp(c, e, 500)
|
|
}
|
|
return
|
|
}
|
|
if t != nil {
|
|
tasks = append(tasks, t)
|
|
}
|
|
}
|
|
common.SuccessResp(c, gin.H{
|
|
"task": getTaskInfos(tasks),
|
|
})
|
|
}
|
|
|
|
func ArchiveDown(c *gin.Context) {
|
|
archiveRawPath := c.MustGet("path").(string)
|
|
innerPath := utils.FixAndCleanPath(c.Query("inner"))
|
|
password := c.Query("pass")
|
|
filename := stdpath.Base(innerPath)
|
|
storage, err := fs.GetStorage(archiveRawPath, &fs.GetStoragesArgs{})
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 500)
|
|
return
|
|
}
|
|
if common.ShouldProxy(storage, filename) {
|
|
ArchiveProxy(c)
|
|
return
|
|
} else {
|
|
link, _, err := fs.ArchiveDriverExtract(c, archiveRawPath, model.ArchiveInnerArgs{
|
|
ArchiveArgs: model.ArchiveArgs{
|
|
LinkArgs: model.LinkArgs{
|
|
IP: c.ClientIP(),
|
|
Header: c.Request.Header,
|
|
Type: c.Query("type"),
|
|
HttpReq: c.Request,
|
|
Redirect: true,
|
|
},
|
|
Password: password,
|
|
},
|
|
InnerPath: innerPath,
|
|
})
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 500)
|
|
return
|
|
}
|
|
down(c, link)
|
|
}
|
|
}
|
|
|
|
func ArchiveProxy(c *gin.Context) {
|
|
archiveRawPath := c.MustGet("path").(string)
|
|
innerPath := utils.FixAndCleanPath(c.Query("inner"))
|
|
password := c.Query("pass")
|
|
filename := stdpath.Base(innerPath)
|
|
storage, err := fs.GetStorage(archiveRawPath, &fs.GetStoragesArgs{})
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 500)
|
|
return
|
|
}
|
|
if canProxy(storage, filename) {
|
|
// TODO: Support external download proxy URL
|
|
link, file, err := fs.ArchiveDriverExtract(c, archiveRawPath, model.ArchiveInnerArgs{
|
|
ArchiveArgs: model.ArchiveArgs{
|
|
LinkArgs: model.LinkArgs{
|
|
Header: c.Request.Header,
|
|
Type: c.Query("type"),
|
|
HttpReq: c.Request,
|
|
},
|
|
Password: password,
|
|
},
|
|
InnerPath: innerPath,
|
|
})
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 500)
|
|
return
|
|
}
|
|
localProxy(c, link, file, storage.GetStorage().ProxyRange)
|
|
} else {
|
|
common.ErrorStrResp(c, "proxy not allowed", 403)
|
|
return
|
|
}
|
|
}
|
|
|
|
func ArchiveInternalExtract(c *gin.Context) {
|
|
archiveRawPath := c.MustGet("path").(string)
|
|
innerPath := utils.FixAndCleanPath(c.Query("inner"))
|
|
password := c.Query("pass")
|
|
rc, size, err := fs.ArchiveInternalExtract(c, archiveRawPath, model.ArchiveInnerArgs{
|
|
ArchiveArgs: model.ArchiveArgs{
|
|
LinkArgs: model.LinkArgs{
|
|
Header: c.Request.Header,
|
|
Type: c.Query("type"),
|
|
HttpReq: c.Request,
|
|
},
|
|
Password: password,
|
|
},
|
|
InnerPath: innerPath,
|
|
})
|
|
if err != nil {
|
|
common.ErrorResp(c, err, 500)
|
|
return
|
|
}
|
|
defer func() {
|
|
if err := rc.Close(); err != nil {
|
|
log.Errorf("failed to close file streamer, %v", err)
|
|
}
|
|
}()
|
|
headers := map[string]string{
|
|
"Referrer-Policy": "no-referrer",
|
|
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
|
|
}
|
|
filename := stdpath.Base(innerPath)
|
|
headers["Content-Disposition"] = fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, filename, url.PathEscape(filename))
|
|
contentType := c.Request.Header.Get("Content-Type")
|
|
if contentType == "" {
|
|
contentType = utils.GetMimeType(filename)
|
|
}
|
|
c.DataFromReader(200, size, contentType, rc, headers)
|
|
}
|
|
|
|
func ArchiveExtensions(c *gin.Context) {
|
|
var ext []string
|
|
for key := range tool.Tools {
|
|
ext = append(ext, key)
|
|
}
|
|
common.SuccessResp(c, ext)
|
|
}
|