945 lines
21 KiB
Go
945 lines
21 KiB
Go
package fileshdr
|
||
|
||
import (
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"os"
|
||
"path"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/ihexxa/gocfg"
|
||
"github.com/ihexxa/multipart"
|
||
|
||
"github.com/ihexxa/quickshare/src/depidx"
|
||
q "github.com/ihexxa/quickshare/src/handlers"
|
||
"github.com/ihexxa/quickshare/src/userstore"
|
||
"github.com/ihexxa/quickshare/src/worker/localworker"
|
||
)
|
||
|
||
var (
|
||
// queries
|
||
FilePathQuery = "fp"
|
||
ListDirQuery = "dp"
|
||
|
||
// headers
|
||
rangeHeader = "Range"
|
||
acceptRangeHeader = "Accept-Range"
|
||
ifRangeHeader = "If-Range"
|
||
keepAliveHeader = "Keep-Alive"
|
||
connectionHeader = "Connection"
|
||
)
|
||
|
||
type FileHandlers struct {
|
||
cfg gocfg.ICfg
|
||
deps *depidx.Deps
|
||
uploadMgr *UploadMgr
|
||
}
|
||
|
||
func NewFileHandlers(cfg gocfg.ICfg, deps *depidx.Deps) (*FileHandlers, error) {
|
||
handlers := &FileHandlers{
|
||
cfg: cfg,
|
||
deps: deps,
|
||
uploadMgr: NewUploadMgr(deps.KV()),
|
||
}
|
||
deps.Workers().AddHandler(MsgTypeSha1, handlers.genSha1)
|
||
|
||
return handlers, nil
|
||
}
|
||
|
||
type AutoLocker struct {
|
||
h *FileHandlers
|
||
c *gin.Context
|
||
key string
|
||
}
|
||
|
||
func (h *FileHandlers) NewAutoLocker(c *gin.Context, key string) *AutoLocker {
|
||
return &AutoLocker{
|
||
h: h,
|
||
c: c,
|
||
key: key,
|
||
}
|
||
}
|
||
|
||
func (lk *AutoLocker) Exec(handler func()) {
|
||
var err error
|
||
kv := lk.h.deps.KV()
|
||
locked := false
|
||
|
||
defer func() {
|
||
if p := recover(); p != nil {
|
||
lk.h.deps.Log().Error(p)
|
||
}
|
||
if locked {
|
||
if err = kv.Unlock(lk.key); err != nil {
|
||
lk.h.deps.Log().Error(err)
|
||
}
|
||
}
|
||
}()
|
||
|
||
if err = kv.TryLock(lk.key); err != nil {
|
||
lk.c.JSON(q.ErrResp(lk.c, 500, errors.New("fail to lock the file")))
|
||
return
|
||
}
|
||
|
||
locked = true
|
||
handler()
|
||
}
|
||
|
||
// related elements: role, user, action(listing, downloading)/sharing
|
||
func (h *FileHandlers) canAccess(userName, role, op, sharedPath string) bool {
|
||
if role == userstore.AdminRole {
|
||
return true
|
||
}
|
||
|
||
// the file path must start with userName: <userName>/...
|
||
parts := strings.Split(sharedPath, "/")
|
||
if len(parts) < 2 { // the path must be longer than <userName>/files
|
||
return false
|
||
} else if parts[0] == userName {
|
||
return true
|
||
}
|
||
|
||
// check if it is shared
|
||
// TODO: find a better approach
|
||
if op != "list" && op != "download" {
|
||
return false
|
||
}
|
||
|
||
_, ok := h.deps.FileInfos().GetSharing(sharedPath)
|
||
return ok
|
||
}
|
||
|
||
type CreateReq struct {
|
||
Path string `json:"path"`
|
||
FileSize int64 `json:"fileSize"`
|
||
}
|
||
|
||
func (h *FileHandlers) Create(c *gin.Context) {
|
||
req := &CreateReq{}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
role := c.MustGet(q.RoleParam).(string)
|
||
userName := c.MustGet(q.UserParam).(string)
|
||
if !h.canAccess(userName, role, "create", req.Path) {
|
||
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
|
||
return
|
||
}
|
||
|
||
userID := c.MustGet(q.UserIDParam).(string)
|
||
userIDInt, err := strconv.ParseUint(userID, 10, 64)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
tmpFilePath := q.UploadPath(userName, req.Path)
|
||
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
||
locker.Exec(func() {
|
||
err = h.deps.Users().SetUsed(userIDInt, true, req.FileSize)
|
||
if err != nil {
|
||
if userstore.IsReachedLimitErr(err) {
|
||
c.JSON(q.ErrResp(c, 429, err))
|
||
} else {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
}
|
||
return
|
||
}
|
||
|
||
err = h.deps.FS().Create(tmpFilePath)
|
||
if err != nil {
|
||
if os.IsExist(err) {
|
||
// avoid adding file size more than once
|
||
err = h.deps.Users().SetUsed(userIDInt, false, req.FileSize)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
} else {
|
||
c.JSON(q.ErrResp(c, 304, err))
|
||
}
|
||
} else {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
}
|
||
return
|
||
}
|
||
err = h.uploadMgr.AddInfo(userID, req.Path, tmpFilePath, req.FileSize)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
err = h.deps.FS().MkdirAll(filepath.Dir(req.Path))
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
c.JSON(q.Resp(200))
|
||
})
|
||
}
|
||
|
||
func (h *FileHandlers) Delete(c *gin.Context) {
|
||
filePath := c.Query(FilePathQuery)
|
||
if filePath == "" {
|
||
c.JSON(q.ErrResp(c, 400, errors.New("invalid file path")))
|
||
return
|
||
}
|
||
role := c.MustGet(q.RoleParam).(string)
|
||
userName := c.MustGet(q.UserParam).(string)
|
||
if !h.canAccess(userName, role, "delete", filePath) {
|
||
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
|
||
return
|
||
}
|
||
|
||
userID := c.MustGet(q.UserIDParam).(string)
|
||
userIDInt, err := strconv.ParseUint(userID, 10, 64)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
locker := h.NewAutoLocker(c, lockName(filePath))
|
||
locker.Exec(func() {
|
||
info, err := h.deps.FS().Stat(filePath)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
err = h.deps.FS().Remove(filePath)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
err = h.deps.Users().SetUsed(userIDInt, false, info.Size())
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
c.JSON(q.Resp(200))
|
||
})
|
||
}
|
||
|
||
type MetadataResp struct {
|
||
Name string `json:"name"`
|
||
Size int64 `json:"size"`
|
||
ModTime time.Time `json:"modTime"`
|
||
IsDir bool `json:"isDir"`
|
||
Sha1 string `json:"sha1"`
|
||
}
|
||
|
||
func (h *FileHandlers) Metadata(c *gin.Context) {
|
||
filePath := c.Query(FilePathQuery)
|
||
if filePath == "" {
|
||
c.JSON(q.ErrResp(c, 400, errors.New("invalid file path")))
|
||
return
|
||
}
|
||
role := c.MustGet(q.RoleParam).(string)
|
||
userName := c.MustGet(q.UserParam).(string)
|
||
if !h.canAccess(userName, role, "metadata", filePath) {
|
||
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
|
||
return
|
||
}
|
||
|
||
info, err := h.deps.FS().Stat(filePath)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
c.JSON(200, MetadataResp{
|
||
Name: info.Name(),
|
||
Size: info.Size(),
|
||
ModTime: info.ModTime(),
|
||
IsDir: info.IsDir(),
|
||
})
|
||
}
|
||
|
||
type MkdirReq struct {
|
||
Path string `json:"path"`
|
||
}
|
||
|
||
func (h *FileHandlers) Mkdir(c *gin.Context) {
|
||
req := &MkdirReq{}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(q.ErrResp(c, 400, err))
|
||
return
|
||
}
|
||
role := c.MustGet(q.RoleParam).(string)
|
||
userName := c.MustGet(q.UserParam).(string)
|
||
if !h.canAccess(userName, role, "mkdir", req.Path) {
|
||
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
|
||
return
|
||
}
|
||
|
||
err := h.deps.FS().MkdirAll(req.Path)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
c.JSON(q.Resp(200))
|
||
}
|
||
|
||
type MoveReq struct {
|
||
OldPath string `json:"oldPath"`
|
||
NewPath string `json:"newPath"`
|
||
}
|
||
|
||
func (h *FileHandlers) Move(c *gin.Context) {
|
||
req := &MoveReq{}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(q.ErrResp(c, 400, err))
|
||
return
|
||
}
|
||
role := c.MustGet(q.RoleParam).(string)
|
||
userName := c.MustGet(q.UserParam).(string)
|
||
if !h.canAccess(userName, role, "move", req.OldPath) || !h.canAccess(userName, role, "move", req.NewPath) {
|
||
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
|
||
return
|
||
}
|
||
|
||
_, err := h.deps.FS().Stat(req.OldPath)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
_, err = h.deps.FS().Stat(req.NewPath)
|
||
if err != nil && !os.IsNotExist(err) {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
} else if err == nil {
|
||
// err is nil because file exists
|
||
c.JSON(q.ErrResp(c, 400, os.ErrExist))
|
||
return
|
||
}
|
||
|
||
err = h.deps.FS().Rename(req.OldPath, req.NewPath)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
c.JSON(q.Resp(200))
|
||
}
|
||
|
||
type UploadChunkReq struct {
|
||
Path string `json:"path"`
|
||
Content string `json:"content"`
|
||
Offset int64 `json:"offset"`
|
||
}
|
||
|
||
func (h *FileHandlers) UploadChunk(c *gin.Context) {
|
||
req := &UploadChunkReq{}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
role := c.MustGet(q.RoleParam).(string)
|
||
userID := c.MustGet(q.UserIDParam).(string)
|
||
userName := c.MustGet(q.UserParam).(string)
|
||
if !h.canAccess(userName, role, "upload.chunk", req.Path) {
|
||
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
|
||
return
|
||
}
|
||
|
||
userIDInt, err := strconv.ParseUint(userID, 10, 64)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
ok, err := h.deps.Limiter().CanWrite(userIDInt, len([]byte(req.Content)))
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
} else if !ok {
|
||
c.JSON(q.ErrResp(c, 429, errors.New("retry later")))
|
||
return
|
||
}
|
||
|
||
tmpFilePath := q.UploadPath(userName, req.Path)
|
||
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
||
locker.Exec(func() {
|
||
var err error
|
||
|
||
_, fileSize, uploaded, err := h.uploadMgr.GetInfo(userID, tmpFilePath)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
} else if uploaded != req.Offset {
|
||
c.JSON(q.ErrResp(c, 500, errors.New("offset != uploaded")))
|
||
return
|
||
}
|
||
|
||
content, err := base64.StdEncoding.DecodeString(req.Content)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
wrote, err := h.deps.FS().WriteAt(tmpFilePath, []byte(content), req.Offset)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
err = h.uploadMgr.SetInfo(userID, tmpFilePath, req.Offset+int64(wrote))
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
// move the file from uploading dir to uploaded dir
|
||
if uploaded+int64(wrote) == fileSize {
|
||
fsFilePath, err := h.getFSFilePath(userID, req.Path)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
err = h.deps.FS().Rename(tmpFilePath, fsFilePath)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, fmt.Errorf("%s error: %w", req.Path, err)))
|
||
return
|
||
}
|
||
err = h.uploadMgr.DelInfo(userID, tmpFilePath)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
msg, err := json.Marshal(Sha1Params{
|
||
FilePath: fsFilePath,
|
||
})
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
err = h.deps.Workers().TryPut(
|
||
localworker.NewMsg(
|
||
h.deps.ID().Gen(),
|
||
map[string]string{localworker.MsgTypeKey: MsgTypeSha1},
|
||
string(msg),
|
||
),
|
||
)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
}
|
||
|
||
c.JSON(200, &UploadStatusResp{
|
||
Path: req.Path,
|
||
IsDir: false,
|
||
FileSize: fileSize,
|
||
Uploaded: uploaded + int64(wrote),
|
||
})
|
||
})
|
||
}
|
||
|
||
func (h *FileHandlers) getFSFilePath(userID, fsFilePath string) (string, error) {
|
||
_, err := h.deps.FS().Stat(fsFilePath)
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
return fsFilePath, nil
|
||
}
|
||
return "", err
|
||
}
|
||
|
||
// this file exists
|
||
maxDetect := 1024
|
||
for i := 1; i < maxDetect; i++ {
|
||
dir := path.Dir(fsFilePath)
|
||
nameAndExt := path.Base(fsFilePath)
|
||
ext := path.Ext(nameAndExt)
|
||
fileName := nameAndExt[:len(nameAndExt)-len(ext)]
|
||
|
||
detectPath := path.Join(dir, fmt.Sprintf("%s_%d%s", fileName, i, ext))
|
||
_, err := h.deps.FS().Stat(detectPath)
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
return detectPath, nil
|
||
} else {
|
||
return "", err
|
||
}
|
||
}
|
||
}
|
||
|
||
return "", fmt.Errorf("found more than %d duplicated files", maxDetect)
|
||
}
|
||
|
||
type UploadStatusResp struct {
|
||
Path string `json:"path"`
|
||
IsDir bool `json:"isDir"`
|
||
FileSize int64 `json:"fileSize"`
|
||
Uploaded int64 `json:"uploaded"`
|
||
}
|
||
|
||
func (h *FileHandlers) UploadStatus(c *gin.Context) {
|
||
filePath := c.Query(FilePathQuery)
|
||
if filePath == "" {
|
||
c.JSON(q.ErrResp(c, 400, errors.New("invalid file name")))
|
||
}
|
||
role := c.MustGet(q.RoleParam).(string)
|
||
userName := c.MustGet(q.UserParam).(string)
|
||
if !h.canAccess(userName, role, "upload.status", filePath) {
|
||
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
|
||
return
|
||
}
|
||
|
||
userID := c.MustGet(q.UserIDParam).(string)
|
||
tmpFilePath := q.UploadPath(userName, filePath)
|
||
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
||
locker.Exec(func() {
|
||
_, fileSize, uploaded, err := h.uploadMgr.GetInfo(userID, tmpFilePath)
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
c.JSON(q.ErrResp(c, 404, err))
|
||
} else {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
}
|
||
return
|
||
}
|
||
|
||
c.JSON(200, &UploadStatusResp{
|
||
Path: filePath,
|
||
IsDir: false,
|
||
FileSize: fileSize,
|
||
Uploaded: uploaded,
|
||
})
|
||
})
|
||
}
|
||
|
||
// TODO: support ETag
|
||
func (h *FileHandlers) Download(c *gin.Context) {
|
||
rangeVal := c.GetHeader(rangeHeader)
|
||
ifRangeVal := c.GetHeader(ifRangeHeader)
|
||
filePath := c.Query(FilePathQuery)
|
||
if filePath == "" {
|
||
c.JSON(q.ErrResp(c, 400, errors.New("invalid file name")))
|
||
return
|
||
}
|
||
role := c.MustGet(q.RoleParam).(string)
|
||
userName := c.MustGet(q.UserParam).(string)
|
||
if !h.canAccess(userName, role, "download", filePath) {
|
||
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
|
||
return
|
||
}
|
||
userID := c.MustGet(q.UserIDParam).(string)
|
||
userIDInt, err := strconv.ParseUint(userID, 10, 64)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
// TODO: when sharing is introduced, move following logics to a separeted method
|
||
// concurrently file accessing is managed by os
|
||
info, err := h.deps.FS().Stat(filePath)
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
c.JSON(q.ErrResp(c, 404, os.ErrNotExist))
|
||
} else {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
}
|
||
return
|
||
} else if info.IsDir() {
|
||
c.JSON(q.ErrResp(c, 400, errors.New("downloading a folder is not supported")))
|
||
return
|
||
}
|
||
|
||
// https://golang.google.cn/pkg/net/http/#DetectContentType
|
||
// DetectContentType considers at most the first 512 bytes of data.
|
||
fileHeadBuf := make([]byte, 512)
|
||
read, err := h.deps.FS().ReadAt(filePath, fileHeadBuf, 0)
|
||
if err != nil && err != io.EOF {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
contentType := http.DetectContentType(fileHeadBuf[:read])
|
||
|
||
fd, id, err := h.deps.FS().GetFileReader(filePath)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
defer func() {
|
||
err := h.deps.FS().CloseReader(fmt.Sprint(id))
|
||
if err != nil {
|
||
h.deps.Log().Errorf("failed to close: %s", err)
|
||
}
|
||
}()
|
||
|
||
extraHeaders := map[string]string{
|
||
"Content-Disposition": fmt.Sprintf(`attachment; filename="%s"`, info.Name()),
|
||
}
|
||
|
||
// respond to normal requests
|
||
if ifRangeVal != "" || rangeVal == "" {
|
||
limitedReader, err := h.GetStreamReader(userIDInt, fd)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
c.DataFromReader(200, info.Size(), contentType, limitedReader, extraHeaders)
|
||
return
|
||
}
|
||
|
||
// respond to range requests
|
||
parts, err := multipart.RangeToParts(rangeVal, contentType, fmt.Sprintf("%d", info.Size()))
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 400, err))
|
||
return
|
||
}
|
||
|
||
mw, contentLength, err := multipart.NewResponseWriter(fd, parts, false)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
// TODO: reader will be closed by multipart response writer?
|
||
go mw.Write()
|
||
|
||
limitedReader, err := h.GetStreamReader(userIDInt, mw)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
// it takes the \r\n before body into account, so contentLength+2
|
||
c.DataFromReader(206, contentLength+2, contentType, limitedReader, extraHeaders)
|
||
}
|
||
|
||
type ListResp struct {
|
||
Cwd string `json:"cwd"`
|
||
Metadatas []*MetadataResp `json:"metadatas"`
|
||
}
|
||
|
||
func (h *FileHandlers) MergeFileInfos(dirPath string, infos []os.FileInfo) ([]*MetadataResp, error) {
|
||
filePaths := []string{}
|
||
metadatas := []*MetadataResp{}
|
||
for _, info := range infos {
|
||
if !info.IsDir() {
|
||
filePaths = append(filePaths, filepath.Join(dirPath, info.Name()))
|
||
}
|
||
metadatas = append(metadatas, &MetadataResp{
|
||
Name: info.Name(),
|
||
Size: info.Size(),
|
||
ModTime: info.ModTime(),
|
||
IsDir: info.IsDir(),
|
||
})
|
||
}
|
||
|
||
dbInfos, err := h.deps.FileInfos().GetInfos(filePaths)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, metadata := range metadatas {
|
||
if !metadata.IsDir {
|
||
dbInfo, ok := dbInfos[filepath.Join(dirPath, metadata.Name)]
|
||
if ok {
|
||
metadata.Sha1 = dbInfo.Sha1
|
||
}
|
||
}
|
||
}
|
||
|
||
return metadatas, nil
|
||
}
|
||
|
||
func (h *FileHandlers) List(c *gin.Context) {
|
||
dirPath := c.Query(ListDirQuery)
|
||
if dirPath == "" {
|
||
c.JSON(q.ErrResp(c, 400, errors.New("incorrect path name")))
|
||
return
|
||
}
|
||
role := c.MustGet(q.RoleParam).(string)
|
||
userName := c.MustGet(q.UserParam).(string)
|
||
if !h.canAccess(userName, role, "list", dirPath) {
|
||
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
|
||
return
|
||
}
|
||
|
||
infos, err := h.deps.FS().ListDir(dirPath)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
metadatas, err := h.MergeFileInfos(dirPath, infos)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
c.JSON(200, &ListResp{
|
||
Cwd: dirPath,
|
||
Metadatas: metadatas,
|
||
})
|
||
}
|
||
|
||
func (h *FileHandlers) ListHome(c *gin.Context) {
|
||
userName := c.MustGet(q.UserParam).(string)
|
||
fsPath := q.FsRootPath(userName, "/")
|
||
|
||
infos, err := h.deps.FS().ListDir(fsPath)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
metadatas, err := h.MergeFileInfos(fsPath, infos)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
c.JSON(200, &ListResp{
|
||
Cwd: fsPath,
|
||
Metadatas: metadatas,
|
||
})
|
||
}
|
||
|
||
func (h *FileHandlers) Copy(c *gin.Context) {
|
||
c.JSON(q.NewMsgResp(501, "Not Implemented"))
|
||
}
|
||
|
||
func (h *FileHandlers) CopyDir(c *gin.Context) {
|
||
c.JSON(q.NewMsgResp(501, "Not Implemented"))
|
||
}
|
||
|
||
func lockName(filePath string) string {
|
||
return filePath
|
||
}
|
||
|
||
type ListUploadingsResp struct {
|
||
UploadInfos []*UploadInfo `json:"uploadInfos"`
|
||
}
|
||
|
||
func (h *FileHandlers) ListUploadings(c *gin.Context) {
|
||
userID := c.MustGet(q.UserIDParam).(string)
|
||
|
||
infos, err := h.uploadMgr.ListInfo(userID)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
if infos == nil {
|
||
infos = []*UploadInfo{}
|
||
}
|
||
c.JSON(200, &ListUploadingsResp{UploadInfos: infos})
|
||
}
|
||
|
||
func (h *FileHandlers) DelUploading(c *gin.Context) {
|
||
filePath := c.Query(FilePathQuery)
|
||
if filePath == "" {
|
||
c.JSON(q.ErrResp(c, 400, errors.New("invalid file path")))
|
||
return
|
||
}
|
||
|
||
userID := c.MustGet(q.UserIDParam).(string)
|
||
userIDInt, err := strconv.ParseUint(userID, 10, 64)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
userName := c.MustGet(q.UserParam).(string)
|
||
tmpFilePath := q.UploadPath(userName, filePath)
|
||
locker := h.NewAutoLocker(c, lockName(tmpFilePath))
|
||
locker.Exec(func() {
|
||
_, size, _, err := h.uploadMgr.GetInfo(userID, tmpFilePath)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
err = h.deps.FS().Remove(tmpFilePath)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
err = h.deps.Users().SetUsed(userIDInt, false, size)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
err = h.uploadMgr.DelInfo(userID, tmpFilePath)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
c.JSON(q.Resp(200))
|
||
})
|
||
}
|
||
|
||
type SharingReq struct {
|
||
SharingPath string `json:"sharingPath"`
|
||
}
|
||
|
||
func (h *FileHandlers) AddSharing(c *gin.Context) {
|
||
req := &SharingReq{}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(q.ErrResp(c, 400, err))
|
||
return
|
||
}
|
||
|
||
// TODO: move canAccess to authedFS
|
||
role := c.MustGet(q.RoleParam).(string)
|
||
userName := c.MustGet(q.UserParam).(string)
|
||
// op is empty, because users must be admin, or the path belongs to this user
|
||
if !h.canAccess(userName, role, "", req.SharingPath) {
|
||
c.JSON(q.ErrResp(c, 403, errors.New("forbidden")))
|
||
return
|
||
}
|
||
|
||
err := h.deps.FileInfos().AddSharing(req.SharingPath)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
c.JSON(q.Resp(200))
|
||
}
|
||
|
||
func (h *FileHandlers) DelSharing(c *gin.Context) {
|
||
dirPath := c.Query(FilePathQuery)
|
||
if dirPath == "" {
|
||
c.JSON(q.ErrResp(c, 400, errors.New("invalid file path")))
|
||
return
|
||
}
|
||
|
||
// TODO: move canAccess to authedFS
|
||
userName := c.MustGet(q.UserParam).(string)
|
||
role := c.MustGet(q.RoleParam).(string)
|
||
if !h.canAccess(userName, role, "", dirPath) {
|
||
c.JSON(q.ErrResp(c, 403, errors.New("forbidden")))
|
||
return
|
||
}
|
||
|
||
err := h.deps.FileInfos().DelSharing(dirPath)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
c.JSON(q.Resp(200))
|
||
}
|
||
|
||
func (h *FileHandlers) IsSharing(c *gin.Context) {
|
||
dirPath := c.Query(FilePathQuery)
|
||
if dirPath == "" {
|
||
c.JSON(q.ErrResp(c, 400, errors.New("invalid file path")))
|
||
return
|
||
}
|
||
|
||
exist, ok := h.deps.FileInfos().GetSharing(dirPath)
|
||
if exist && ok {
|
||
c.JSON(q.Resp(200))
|
||
} else {
|
||
c.JSON(q.Resp(404))
|
||
}
|
||
}
|
||
|
||
type SharingResp struct {
|
||
SharingDirs []string `json:"sharingDirs"`
|
||
}
|
||
|
||
func (h *FileHandlers) ListSharings(c *gin.Context) {
|
||
// TODO: move canAccess to authedFS
|
||
userName := c.MustGet(q.UserParam).(string)
|
||
|
||
sharingDirs, err := h.deps.FileInfos().ListSharings(q.FsRootPath(userName, "/"))
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
dirs := []string{}
|
||
for sharingDir := range sharingDirs {
|
||
dirs = append(dirs, sharingDir)
|
||
}
|
||
c.JSON(200, &SharingResp{SharingDirs: dirs})
|
||
}
|
||
|
||
type HashBody struct {
|
||
FilePath string `json:"filePath"`
|
||
}
|
||
|
||
func (h *FileHandlers) GenerateHash(c *gin.Context) {
|
||
req := &HashBody{}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(q.ErrResp(c, 400, err))
|
||
return
|
||
}
|
||
|
||
role := c.MustGet(q.RoleParam).(string)
|
||
userName := c.MustGet(q.UserParam).(string)
|
||
if !h.canAccess(userName, role, "hash.gen", req.FilePath) {
|
||
c.JSON(q.ErrResp(c, 403, q.ErrAccessDenied))
|
||
return
|
||
}
|
||
|
||
msg, err := json.Marshal(Sha1Params{
|
||
FilePath: req.FilePath,
|
||
})
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
|
||
err = h.deps.Workers().TryPut(
|
||
localworker.NewMsg(
|
||
h.deps.ID().Gen(),
|
||
map[string]string{localworker.MsgTypeKey: MsgTypeSha1},
|
||
string(msg),
|
||
),
|
||
)
|
||
if err != nil {
|
||
c.JSON(q.ErrResp(c, 500, err))
|
||
return
|
||
}
|
||
c.JSON(q.Resp(200))
|
||
}
|
||
|
||
func (h *FileHandlers) GetStreamReader(userID uint64, fd io.Reader) (io.Reader, error) {
|
||
pr, pw := io.Pipe()
|
||
chunkSize := 100 * 1024 // notice: it can not be greater than limiter's token count
|
||
|
||
go func() {
|
||
defer pw.Close()
|
||
|
||
for {
|
||
ok, err := h.deps.Limiter().CanRead(userID, chunkSize)
|
||
if err != nil {
|
||
pw.CloseWithError(err)
|
||
break
|
||
} else if !ok {
|
||
time.Sleep(time.Duration(1) * time.Second)
|
||
continue
|
||
}
|
||
|
||
_, err = io.CopyN(pw, fd, int64(chunkSize))
|
||
if err != nil {
|
||
if err != io.EOF {
|
||
pw.CloseWithError(err)
|
||
}
|
||
break
|
||
}
|
||
}
|
||
}()
|
||
|
||
return pr, nil
|
||
}
|