feat(qs2) add qs2 framework

This commit is contained in:
hexxa 2020-12-05 10:30:03 +08:00
parent 6ae65fe09b
commit 83100007e3
33 changed files with 2934 additions and 60 deletions

View file

@ -1,12 +0,0 @@
package handlers
import (
"github.com/gin-gonic/gin"
)
func Upload(ctx *gin.Context) {}
func List(ctx *gin.Context) {}
func Delete(ctx *gin.Context) {}
func Metadata(ctx *gin.Context) {}
func Copy(ctx *gin.Context) {}
func Move(ctx *gin.Context) {}

View file

@ -0,0 +1,414 @@
package fileshdr
import (
"crypto/sha1"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"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"
)
var (
// dirs
UploadDir = "uploadings"
FsDir = "files"
// queries
FilePathQuery = "fp"
ListDirQuery = "dp"
// headers
rangeHeader = "Range"
acceptRangeHeader = "Accept-Range"
ifRangeHeader = "If-Range"
)
type FileHandlers struct {
cfg gocfg.ICfg
deps *depidx.Deps
uploadMgr *UploadMgr
}
func NewFileHandlers(cfg gocfg.ICfg, deps *depidx.Deps) (*FileHandlers, error) {
var err error
if err = deps.FS().MkdirAll(UploadDir); err != nil {
return nil, err
}
if err = deps.FS().MkdirAll(FsDir); err != nil {
return nil, err
}
return &FileHandlers{
cfg: cfg,
deps: deps,
uploadMgr: NewUploadMgr(deps.KV()),
}, err
}
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()
if err = kv.TryLock(lk.key); err != nil {
lk.c.JSON(q.Resp(500))
return
}
handler()
if err = kv.Unlock(lk.key); err != nil {
// TODO: use logger
fmt.Println(err)
}
}
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
}
tmpFilePath := h.GetTmpPath(req.Path)
locker := h.NewAutoLocker(c, tmpFilePath)
locker.Exec(func() {
err := h.deps.FS().Create(tmpFilePath)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
err = h.uploadMgr.AddInfo(req.Path, tmpFilePath, req.FileSize, false)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
fileDir := h.FsPath(filepath.Dir(req.Path))
err = h.deps.FS().MkdirAll(fileDir)
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
}
filePath = h.FsPath(filePath)
err := h.deps.FS().Remove(filePath)
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"`
}
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
}
filePath = h.FsPath(filePath)
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
}
dirPath := h.FsPath(req.Path)
err := h.deps.FS().MkdirAll(dirPath)
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
}
oldPath := h.FsPath(req.OldPath)
newPath := h.FsPath(req.NewPath)
_, err := h.deps.FS().Stat(oldPath)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
_, err = h.deps.FS().Stat(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(oldPath, 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
}
tmpFilePath := h.GetTmpPath(req.Path)
locker := h.NewAutoLocker(c, tmpFilePath)
locker.Exec(func() {
var err error
_, fileSize, uploaded, err := h.uploadMgr.GetInfo(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
}
wrote, err := h.deps.FS().WriteAt(tmpFilePath, []byte(req.Content), req.Offset)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
err = h.uploadMgr.IncreUploaded(tmpFilePath, 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 := h.FsPath(req.Path)
err = h.deps.FS().Rename(tmpFilePath, fsFilePath)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
err = h.uploadMgr.DelInfo(tmpFilePath)
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),
})
})
}
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")))
}
tmpFilePath := h.GetTmpPath(filePath)
locker := h.NewAutoLocker(c, tmpFilePath)
locker.Exec(func() {
_, fileSize, uploaded, err := h.uploadMgr.GetInfo(tmpFilePath)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
c.JSON(200, &UploadStatusResp{
Path: filePath,
IsDir: false,
FileSize: fileSize,
Uploaded: uploaded,
})
})
}
// TODO: support ETag
// TODO: use correct content type
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")))
}
// concurrency relies on os's mechanism
filePath = h.FsPath(filePath)
info, err := h.deps.FS().Stat(filePath)
if err != nil {
c.JSON(q.ErrResp(c, 400, err))
return
} else if info.IsDir() {
c.JSON(q.ErrResp(c, 501, errors.New("downloading a folder is not supported")))
}
r, err := h.deps.FS().GetFileReader(filePath)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
}
// respond to normal requests
if ifRangeVal != "" || rangeVal == "" {
c.DataFromReader(200, info.Size(), "application/octet-stream", r, map[string]string{})
return
}
// respond to range requests
parts, err := multipart.RangeToParts(rangeVal, "application/octet-stream", fmt.Sprintf("%d", info.Size()))
if err != nil {
c.JSON(q.ErrResp(c, 400, err))
}
pr, pw := io.Pipe()
err = multipart.WriteResponse(r, pw, filePath, parts)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
}
extraHeaders := map[string]string{
"Content-Disposition": fmt.Sprintf(`attachment; filename="%s"`, filePath),
}
c.DataFromReader(206, info.Size(), "application/octet-stream", pr, extraHeaders)
}
type ListResp struct {
Metadatas []*MetadataResp `json:"metadatas"`
}
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
}
dirPath = h.FsPath(dirPath)
infos, err := h.deps.FS().ListDir(dirPath)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
metadatas := []*MetadataResp{}
for _, info := range infos {
metadatas = append(metadatas, &MetadataResp{
Name: info.Name(),
Size: info.Size(),
ModTime: info.ModTime(),
IsDir: info.IsDir(),
})
}
c.JSON(200, &ListResp{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 (h *FileHandlers) GetTmpPath(filePath string) string {
return path.Join(UploadDir, fmt.Sprintf("%x", sha1.Sum([]byte(filePath))))
}
func (h *FileHandlers) FsPath(filePath string) string {
return path.Join(FsDir, filePath)
}

View file

@ -0,0 +1,85 @@
package fileshdr
import (
"errors"
"fmt"
"os"
"github.com/ihexxa/quickshare/src/kvstore"
)
var (
isDirKey = "isDir"
fileSizeKey = "fileSize"
uploadedKey = "uploaded"
filePathKey = "fileName"
)
type UploadMgr struct {
kv kvstore.IKVStore
}
func NewUploadMgr(kv kvstore.IKVStore) *UploadMgr {
return &UploadMgr{
kv: kv,
}
}
func (um *UploadMgr) AddInfo(fileName, tmpName string, fileSize int64, isDir bool) error {
err := um.kv.SetInt64(infoKey(tmpName, fileSizeKey), fileSize)
if err != nil {
return err
}
err = um.kv.SetInt64(infoKey(tmpName, uploadedKey), 0)
if err != nil {
return err
}
return um.kv.SetString(infoKey(tmpName, filePathKey), fileName)
}
func (um *UploadMgr) IncreUploaded(fileName string, newUploaded int64) error {
fileSize, ok := um.kv.GetInt64(infoKey(fileName, fileSizeKey))
if !ok {
return fmt.Errorf("file size %s not found", fileName)
}
preUploaded, ok := um.kv.GetInt64(infoKey(fileName, uploadedKey))
if !ok {
return fmt.Errorf("file uploaded %s not found", fileName)
}
if newUploaded+preUploaded <= fileSize {
um.kv.SetInt64(infoKey(fileName, uploadedKey), newUploaded+preUploaded)
return nil
}
return errors.New("uploaded is greater than file size")
}
func (um *UploadMgr) GetInfo(fileName string) (string, int64, int64, error) {
realFilePath, ok := um.kv.GetString(infoKey(fileName, filePathKey))
if !ok {
return "", 0, 0, os.ErrNotExist
}
fileSize, ok := um.kv.GetInt64(infoKey(fileName, fileSizeKey))
if !ok {
return "", 0, 0, os.ErrNotExist
}
uploaded, ok := um.kv.GetInt64(infoKey(fileName, uploadedKey))
if !ok {
return "", 0, 0, os.ErrNotExist
}
return realFilePath, fileSize, uploaded, nil
}
func (um *UploadMgr) DelInfo(fileName string) error {
if err := um.kv.DelInt64(infoKey(fileName, fileSizeKey)); err != nil {
return err
}
if err := um.kv.DelInt64(infoKey(fileName, uploadedKey)); err != nil {
return err
}
return um.kv.DelString(infoKey(fileName, filePathKey))
}
func infoKey(fileName, key string) string {
return fmt.Sprintf("%s:%s", fileName, key)
}

View file

@ -1,9 +0,0 @@
package handlers
import (
"github.com/gin-gonic/gin"
)
func Login(ctx *gin.Context) {}
func Logout(ctx *gin.Context) {}

View file

@ -0,0 +1,75 @@
package singleuserhdr
import (
"errors"
"github.com/gin-gonic/gin"
"github.com/ihexxa/gocfg"
"github.com/ihexxa/quickshare/src/depidx"
q "github.com/ihexxa/quickshare/src/handlers"
)
var ErrInvalidUser = errors.New("invalid user name or password")
type SimpleUserHandlers struct {
cfg gocfg.ICfg
deps *depidx.Deps
}
func NewSimpleUserHandlers(cfg gocfg.ICfg, deps *depidx.Deps) *SimpleUserHandlers {
return &SimpleUserHandlers{
cfg: cfg,
deps: deps,
}
}
func (hdr *SimpleUserHandlers) Login(c *gin.Context) {
userName := c.Query("username")
pwd := c.Query("pwd")
if userName == "" || pwd == "" {
c.JSON(q.ErrResp(c, 400, ErrInvalidUser))
return
}
expectedName, ok1 := hdr.deps.KV().GetString("username")
expectedPwd, ok2 := hdr.deps.KV().GetString("pwd")
if !ok1 || !ok2 {
c.JSON(q.ErrResp(c, 400, ErrInvalidUser))
return
}
if userName != expectedName || pwd != expectedPwd {
c.JSON(q.ErrResp(c, 400, ErrInvalidUser))
return
}
token, err := hdr.deps.Token().ToToken(map[string]string{
"username": expectedName,
})
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
// TODO: use config
c.SetCookie("token", token, 3600, "/", "localhost", false, true)
c.JSON(q.Resp(200))
}
func (hdr *SimpleUserHandlers) Logout(c *gin.Context) {
token, err := c.Cookie("token")
if err != nil {
c.JSON(q.ErrResp(c, 400, err))
return
}
// TODO: // check if token expired
_, err = hdr.deps.Token().FromToken(token, map[string]string{"token": ""})
if err != nil {
c.JSON(q.ErrResp(c, 400, err))
return
}
c.SetCookie("token", "", 0, "/", "localhost", false, true)
c.JSON(q.Resp(200))
}

104
src/handlers/util.go Normal file
View file

@ -0,0 +1,104 @@
package handlers
import (
"fmt"
"github.com/gin-gonic/gin"
)
var statusCodes = map[int]string{
100: "Continue", // RFC 7231, 6.2.1
101: "SwitchingProtocols", // RFC 7231, 6.2.2
102: "Processing", // RFC 2518, 10.1
103: "EarlyHints", // RFC 8297
200: "OK", // RFC 7231, 6.3.1
201: "Created", // RFC 7231, 6.3.2
202: "Accepted", // RFC 7231, 6.3.3
203: "NonAuthoritativeInfo", // RFC 7231, 6.3.4
204: "NoContent", // RFC 7231, 6.3.5
205: "ResetContent", // RFC 7231, 6.3.6
206: "PartialContent", // RFC 7233, 4.1
207: "MultiStatus", // RFC 4918, 11.1
208: "AlreadyReported", // RFC 5842, 7.1
226: "IMUsed", // RFC 3229, 10.4.1
300: "MultipleChoices", // RFC 7231, 6.4.1
301: "MovedPermanently", // RFC 7231, 6.4.2
302: "Found", // RFC 7231, 6.4.3
303: "SeeOther", // RFC 7231, 6.4.4
304: "NotModified", // RFC 7232, 4.1
305: "UseProxy", // RFC 7231, 6.4.5
307: "TemporaryRedirect", // RFC 7231, 6.4.7
308: "PermanentRedirect", // RFC 7538, 3
400: "BadRequest", // RFC 7231, 6.5.1
401: "Unauthorized", // RFC 7235, 3.1
402: "PaymentRequired", // RFC 7231, 6.5.2
403: "Forbidden", // RFC 7231, 6.5.3
404: "NotFound", // RFC 7231, 6.5.4
405: "MethodNotAllowed", // RFC 7231, 6.5.5
406: "NotAcceptable", // RFC 7231, 6.5.6
407: "ProxyAuthRequired", // RFC 7235, 3.2
408: "RequestTimeout", // RFC 7231, 6.5.7
409: "Conflict", // RFC 7231, 6.5.8
410: "Gone", // RFC 7231, 6.5.9
411: "LengthRequired", // RFC 7231, 6.5.10
412: "PreconditionFailed", // RFC 7232, 4.2
413: "RequestEntityTooLarge", // RFC 7231, 6.5.11
414: "RequestURITooLong", // RFC 7231, 6.5.12
415: "UnsupportedMediaType", // RFC 7231, 6.5.13
416: "RequestedRangeNotSatisfiable", // RFC 7233, 4.4
417: "ExpectationFailed", // RFC 7231, 6.5.14
418: "Teapot", // RFC 7168, 2.3.3
421: "MisdirectedRequest", // RFC 7540, 9.1.2
422: "UnprocessableEntity", // RFC 4918, 11.2
423: "Locked", // RFC 4918, 11.3
424: "FailedDependency", // RFC 4918, 11.4
425: "TooEarly", // RFC 8470, 5.2.
426: "UpgradeRequired", // RFC 7231, 6.5.15
428: "PreconditionRequired", // RFC 6585, 3
429: "TooManyRequests", // RFC 6585, 4
431: "RequestHeaderFieldsTooLarge", // RFC 6585, 5
451: "UnavailableForLegalReasons", // RFC 7725, 3
500: "InternalServerError", // RFC 7231, 6.6.1
501: "NotImplemented", // RFC 7231, 6.6.2
502: "BadGateway", // RFC 7231, 6.6.3
503: "ServiceUnavailable", // RFC 7231, 6.6.4
504: "GatewayTimeout", // RFC 7231, 6.6.5
505: "HTTPVersionNotSupported", // RFC 7231, 6.6.6
506: "VariantAlsoNegotiates", // RFC 2295, 8.1
507: "InsufficientStorage", // RFC 4918, 11.5
508: "LoopDetected", // RFC 5842, 7.2
510: "NotExtended", // RFC 2774, 7
511: "NetworkAuthenticationRequired", // RFC 6585, 6
}
type MsgResp struct {
Msg string `json:"msg"`
}
func NewMsgResp(code int, msg string) (int, interface{}) {
_, ok := statusCodes[code]
if !ok {
panic(fmt.Sprintf("status code not found %d", code))
}
return code, &MsgResp{Msg: msg}
}
func Resp(code int) (int, interface{}) {
msg, ok := statusCodes[code]
if !ok {
panic(fmt.Sprintf("status code not found %d", code))
}
return code, &MsgResp{Msg: msg}
}
func ErrResp(c *gin.Context, code int, err error) (int, interface{}) {
_, ok := statusCodes[code]
if !ok {
panic(fmt.Sprintf("status code not found %d", code))
}
gErr := c.Error(err)
return code, gErr.JSON()
}