Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Implement file handling #93

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
184 changes: 184 additions & 0 deletions internal/server/files.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package server

import (
"bufio"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"

"github.com/labstack/echo/v4"
)

type files struct{}

type Resource struct {
RemoteIdentifier string `json:"remoteIdentifier"`
}

type ValetRequestParams struct {
Operation string `json:"operation"`
Resources []Resource
}

type ValetToken struct {
Authorization string `json:"authorization"`
FileId string `json:"fileId"`
Crusader99 marked this conversation as resolved.
Show resolved Hide resolved
}

func (token *ValetToken) GetFilePath() string {
// TODO: Check format of fileId (Security)
// TODO: Allow custom path in config
// TODO: Subfolders for each user (Compatible format with official server)
Crusader99 marked this conversation as resolved.
Show resolved Hide resolved
return "/etc/standardfile/database/" + token.FileId
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have the UserID somewhere to add one more namespace in the path?

Copy link
Contributor Author

@Crusader99 Crusader99 Nov 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UserID could be derived from token.Authorization.

}

// Provides a valet token that is required to execute an operation
func (h *files) ValetTokens(c echo.Context) error {
var params ValetRequestParams
if err := c.Bind(&params); err != nil {
return c.JSON(http.StatusBadRequest, err)
}

if len(params.Resources) != 1 {
return c.JSON(http.StatusBadRequest, "Multi file requests not supported")
Crusader99 marked this conversation as resolved.
Show resolved Hide resolved
}

var token ValetToken
token.Authorization = c.Request().Header.Get(echo.HeaderAuthorization)
token.FileId = params.Resources[0].RemoteIdentifier
valetTokenJson, err := json.Marshal(token)
if err != nil {
return c.JSON(http.StatusBadRequest, err)
}

return c.JSON(http.StatusOK, echo.Map{
"success": true,
"valetToken": base64.StdEncoding.EncodeToString(valetTokenJson),
})
}

// Called before uploading chunks of a file
func (h *files) CreateUploadSession(c echo.Context) error {
valetTokenBase64 := c.Request().Header.Get("x-valet-token")
valetTokenBytes, err := base64.StdEncoding.DecodeString(valetTokenBase64)
if err != nil {
return c.JSON(http.StatusBadRequest, err)
}
valetTokenJson := string(valetTokenBytes)

var token ValetToken
if err := json.Unmarshal([]byte(valetTokenJson), &token); err != nil {
return c.JSON(http.StatusBadRequest, err)
}

fmt.Println("create-session. valet_token: " + valetTokenJson)

if _, err := os.Create(token.GetFilePath()); err != nil {
return c.JSON(http.StatusBadRequest, err)
}

return c.JSON(http.StatusOK, echo.Map{
"success": true,
"uploadId": token.FileId,
})
}

// Called when uploaded all chunks of a file
func (h *files) CloseUploadSession(c echo.Context) error {
valetTokenBase64 := c.Request().Header.Get("x-valet-token")
valetTokenBytes, err := base64.StdEncoding.DecodeString(valetTokenBase64)
if err != nil {
return c.JSON(http.StatusBadRequest, err)
}
valetTokenJson := string(valetTokenBytes)
var token ValetToken
if err := json.Unmarshal([]byte(valetTokenJson), &token); err != nil {
return c.JSON(http.StatusBadRequest, err)
} else if _, err := os.Stat(token.GetFilePath()); errors.Is(err, os.ErrNotExist) {
return c.JSON(http.StatusBadRequest, "File not created")
}

fmt.Println("close-session. valet_token: " + valetTokenJson)
return c.JSON(http.StatusOK, echo.Map{
"success": true,
"message": "File uploaded successfully",
})
}

// Upload parts of a file in an existing session
func (h *files) UploadChunk(c echo.Context) error {
valetTokenBase64 := c.Request().Header.Get("x-valet-token")
valetTokenBytes, err := base64.StdEncoding.DecodeString(valetTokenBase64)
if err != nil {
return c.JSON(http.StatusBadRequest, err)
}
valetTokenJson := string(valetTokenBytes)
var token ValetToken
if err := json.Unmarshal([]byte(valetTokenJson), &token); err != nil {
return c.JSON(http.StatusBadRequest, err)
} else if _, err := os.Stat(token.GetFilePath()); errors.Is(err, os.ErrNotExist) {
return c.JSON(http.StatusBadRequest, "File not created")
}

chunk_id := c.Request().Header.Get("x-chunk-id")
fmt.Println("chunk. valet_token: " + valetTokenJson + " chunk_id: " + chunk_id)

f, err := os.OpenFile(token.GetFilePath(), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return c.JSON(http.StatusBadRequest, err)
}
defer f.Close()

// Create new buffer
writer := bufio.NewWriter(f)
reader := c.Request().Body
if _, err := io.Copy(writer, reader); err != nil {
return c.JSON(http.StatusBadRequest, err)
}

return c.JSON(http.StatusOK, echo.Map{
"success": true,
"message": "Chunk uploaded successfully",
})
}

// Delete file from server
func (h *files) Delete(c echo.Context) error {
valetTokenBase64 := c.Request().Header.Get("x-valet-token")
valetTokenBytes, err := base64.StdEncoding.DecodeString(valetTokenBase64)
if err != nil {
return c.JSON(http.StatusBadRequest, err)
}
valetTokenJson := string(valetTokenBytes)
var token ValetToken
if err := json.Unmarshal([]byte(valetTokenJson), &token); err != nil {
return c.JSON(http.StatusBadRequest, err)
}
err = os.Remove(token.GetFilePath())
if err != nil {
return c.JSON(http.StatusBadRequest, err)
}
return c.JSON(http.StatusOK, echo.Map{
"success": true,
"message": "File removed successfully",
})
}

// Send encrypted file to client
func (h *files) Download(c echo.Context) error {
valetTokenBase64 := c.Request().Header.Get("x-valet-token")
valetTokenBytes, err := base64.StdEncoding.DecodeString(valetTokenBase64)
if err != nil {
return c.JSON(http.StatusBadRequest, err)
}
valetTokenJson := string(valetTokenBytes)
var token ValetToken
if err := json.Unmarshal([]byte(valetTokenJson), &token); err != nil {
return c.JSON(http.StatusBadRequest, err)
}
return c.File(token.GetFilePath())
}
21 changes: 19 additions & 2 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ func EchoEngine(ctrl Controller) *echo.Echo {
engine.Use(middleware.Recover())
// engine.Use(middleware.CSRF()) // not supported by StandardNotes
engine.Use(middleware.Secure())
engine.Use(middleware.CORSWithConfig(middleware.DefaultCORSConfig))

// Expose headers for file download
cors := middleware.DefaultCORSConfig
cors.ExposeHeaders = append(cors.ExposeHeaders, "Content-Range", "Accept-Ranges")
engine.Use(middleware.CORSWithConfig(cors))

engine.Use(middleware.Gzip())

engine.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Expand Down Expand Up @@ -68,6 +73,7 @@ func EchoEngine(ctrl Controller) *echo.Echo {
v1 := router.Group("/v1")
v1restricted := restricted.Group("/v1")

//
// generic handlers
//
router.GET("/version", func(c echo.Context) error {
Expand Down Expand Up @@ -142,7 +148,18 @@ func EchoEngine(ctrl Controller) *echo.Echo {
v2 := router.Group("/v2")
v2.POST("/login", auth.LoginPKCE)
v2.POST("/login-params", auth.ParamsPKCE)
//v2restricted := restricted.Group("/v2")

//
// files
//
files := &files{}
v1restricted.POST("/files/valet-tokens", files.ValetTokens)
// Following endpoints are authorized via valet token
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we should do something like:

v1valet := v1.Group("")
v1valet.Use(a valet middleware for authentication)
v1valet.POST("/files/upload/create-session", files.CreateUploadSession)

v1.POST("/files/upload/create-session", files.CreateUploadSession)
v1.POST("/files/upload/close-session", files.CloseUploadSession)
v1.POST("/files/upload/chunk", files.UploadChunk)
v1.DELETE("/files", files.Delete)
v1.GET("/files", files.Download)

//
// subscription handlers
Expand Down