From 030d0c60d4e9fd4b2820f01da329c0f60df9bbc8 Mon Sep 17 00:00:00 2001 From: Elias Bouassaba Date: Tue, 9 Jul 2024 12:21:07 +0200 Subject: [PATCH] wip(webdav): working Go implementation --- api/main.go | 6 +- conversion/main.go | 6 +- webdav-go/client/api_client.go | 147 +++++++++++++------------ webdav-go/client/idp_client.go | 70 +++++------- webdav-go/go.mod | 25 ----- webdav-go/go.sum | 59 ---------- webdav-go/handler/handler.go | 21 +++- webdav-go/handler/method_copy.go | 57 +++++++++- webdav-go/handler/method_delete.go | 31 +++++- webdav-go/handler/method_get.go | 108 +++++++++++++++++- webdav-go/handler/method_head.go | 30 +++++ webdav-go/handler/method_mkcol.go | 37 +++++++ webdav-go/handler/method_move.go | 62 ++++++++++- webdav-go/handler/method_options.go | 12 +- webdav-go/handler/method_propfind.go | 119 +++++++++++++++++++- webdav-go/handler/method_proppatch.go | 33 +++++- webdav-go/handler/method_put.go | 88 ++++++++++++++- webdav-go/helper/offlice-lock-files.go | 11 ++ webdav-go/helper/path.go | 34 ++++++ webdav-go/helper/pointer.go | 15 --- webdav-go/helper/time.go | 14 +++ webdav-go/helper/token.go | 10 ++ webdav-go/helper/uri.go | 21 ++++ webdav-go/infra/error.go | 65 +++++++++++ webdav-go/infra/token.go | 8 ++ webdav-go/main.go | 42 +++---- webdav/package.json | 2 + webdav/src/server.ts | 1 + 28 files changed, 886 insertions(+), 248 deletions(-) create mode 100644 webdav-go/helper/offlice-lock-files.go create mode 100644 webdav-go/helper/path.go delete mode 100644 webdav-go/helper/pointer.go create mode 100644 webdav-go/helper/time.go create mode 100644 webdav-go/helper/token.go create mode 100644 webdav-go/helper/uri.go create mode 100644 webdav-go/infra/error.go create mode 100644 webdav-go/infra/token.go diff --git a/api/main.go b/api/main.go index b930f35b0..882127028 100644 --- a/api/main.go +++ b/api/main.go @@ -32,13 +32,11 @@ import ( // @BasePath /v2 func main() { if _, err := os.Stat(".env.local"); err == nil { - err := godotenv.Load(".env.local") - if err != nil { + if err := godotenv.Load(".env.local"); err != nil { panic(err) } } else { - err := godotenv.Load() - if err != nil { + if err := godotenv.Load(); err != nil { panic(err) } } diff --git a/conversion/main.go b/conversion/main.go index bde720351..afafe3a25 100644 --- a/conversion/main.go +++ b/conversion/main.go @@ -30,13 +30,11 @@ import ( // @BasePath /v2 func main() { if _, err := os.Stat(".env.local"); err == nil { - err := godotenv.Load(".env.local") - if err != nil { + if err := godotenv.Load(".env.local"); err != nil { panic(err) } } else { - err := godotenv.Load() - if err != nil { + if err := godotenv.Load(); err != nil { panic(err) } } diff --git a/webdav-go/client/api_client.go b/webdav-go/client/api_client.go index c9c6c74e7..12e5ea154 100644 --- a/webdav-go/client/api_client.go +++ b/webdav-go/client/api_client.go @@ -10,26 +10,12 @@ import ( "net/http" "net/url" "os" + "strings" "voltaserve/config" + "voltaserve/helper" "voltaserve/infra" ) -type APIErrorResponse struct { - Code string `json:"code"` - Status int `json:"status"` - Message string `json:"message"` - UserMessage string `json:"userMessage"` - MoreInfo string `json:"moreInfo"` -} - -type APIError struct { - Value APIErrorResponse -} - -func (e *APIError) Error() string { - return fmt.Sprintf("APIError: %v", e.Value) -} - const ( FileTypeFile = "file" FileTypeFolder = "folder" @@ -37,34 +23,16 @@ const ( type APIClient struct { config *config.Config - token *Token + token *infra.Token } -func NewAPIClient(token *Token) *APIClient { +func NewAPIClient(token *infra.Token) *APIClient { return &APIClient{ token: token, config: config.GetConfig(), } } -func (cl *APIClient) GetHealth() (string, error) { - resp, err := http.Get(fmt.Sprintf("%s/v2/health", cl.config.IdPURL)) - if err != nil { - return "", err - } - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - infra.GetLogger().Error(err.Error()) - } - }(resp.Body) - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - return string(body), nil -} - type File struct { ID string `json:"id"` WorkspaceID string `json:"workspaceId"` @@ -75,12 +43,12 @@ type File struct { IsShared bool `json:"isShared"` Snapshot *Snapshot `json:"snapshot,omitempty"` CreateTime string `json:"createTime"` - UpdateTime string `json:"updateTime,omitempty"` + UpdateTime *string `json:"updateTime,omitempty"` } type Snapshot struct { Version int `json:"version"` - Original Download `json:"original"` + Original *Download `json:"original,omitempty"` Preview *Download `json:"preview,omitempty"` OCR *Download `json:"ocr,omitempty"` Text *Download `json:"text,omitempty"` @@ -146,8 +114,7 @@ func (cl *APIClient) CreateFile(opts FileCreateOptions) (*File, error) { return nil, err } var file File - err = json.Unmarshal(body, &file) - if err != nil { + if err = json.Unmarshal(body, &file); err != nil { return nil, err } return &file, nil @@ -200,15 +167,14 @@ func (cl *APIClient) upload(url, method string, blob []byte, name string) (*File return nil, err } var file File - err = json.Unmarshal(respBody, &file) - if err != nil { + if err = json.Unmarshal(respBody, &file); err != nil { return nil, err } return &file, nil } func (cl *APIClient) GetFileByPath(path string) (*File, error) { - req, err := http.NewRequest("GET", fmt.Sprintf("%s/v2/files?path=%s", cl.config.APIURL, url.PathEscape(path)), nil) + req, err := http.NewRequest("GET", fmt.Sprintf("%s/v2/files?path=%s", cl.config.APIURL, helper.EncodeURIComponent(path)), nil) if err != nil { return nil, err } @@ -230,15 +196,14 @@ func (cl *APIClient) GetFileByPath(path string) (*File, error) { return nil, err } var file File - err = json.Unmarshal(body, &file) - if err != nil { + if err = json.Unmarshal(body, &file); err != nil { return nil, err } return &file, nil } func (cl *APIClient) ListFilesByPath(path string) ([]File, error) { - req, err := http.NewRequest("GET", fmt.Sprintf("%s/v2/files/list?path=%s", cl.config.APIURL, url.PathEscape(path)), nil) + req, err := http.NewRequest("GET", fmt.Sprintf("%s/v2/files/list?path=%s", cl.config.APIURL, helper.EncodeURIComponent(path)), nil) if err != nil { return nil, err } @@ -260,8 +225,7 @@ func (cl *APIClient) ListFilesByPath(path string) ([]File, error) { return nil, err } var files []File - err = json.Unmarshal(body, &files) - if err != nil { + if err = json.Unmarshal(body, &files); err != nil { return nil, err } return files, nil @@ -298,8 +262,7 @@ func (cl *APIClient) CopyFile(id string, opts FileCopyOptions) ([]File, error) { return nil, err } var files []File - err = json.Unmarshal(body, &files) - if err != nil { + if err = json.Unmarshal(body, &files); err != nil { return nil, err } return files, nil @@ -331,8 +294,7 @@ func (cl *APIClient) MoveFile(id string, opts FileMoveOptions) error { infra.GetLogger().Error(err.Error()) } }(resp.Body) - _, err = cl.jsonResponseOrThrow(resp) - return err + return cl.successfulResponseOrThrow(resp) } type FileRenameOptions struct { @@ -366,28 +328,27 @@ func (cl *APIClient) PatchFileName(id string, opts FileRenameOptions) (*File, er return nil, err } var file File - err = json.Unmarshal(body, &file) - if err != nil { + if err = json.Unmarshal(body, &file); err != nil { return nil, err } return &file, nil } -func (cl *APIClient) DeleteFile(id string) error { +func (cl *APIClient) DeleteFile(id string) ([]string, error) { b, err := json.Marshal(map[string][]string{"ids": {id}}) if err != nil { - return err + return nil, err } req, err := http.NewRequest("DELETE", fmt.Sprintf("%s/v2/files", cl.config.APIURL), bytes.NewBuffer(b)) if err != nil { - return err + return nil, err } req.Header.Set("Authorization", "Bearer "+cl.token.AccessToken) req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { - return err + return nil, err } defer func(Body io.ReadCloser) { err := Body.Close() @@ -395,8 +356,15 @@ func (cl *APIClient) DeleteFile(id string) error { infra.GetLogger().Error(err.Error()) } }(resp.Body) - _, err = cl.jsonResponseOrThrow(resp) - return err + body, err := cl.jsonResponseOrThrow(resp) + if err != nil { + return nil, err + } + var ids []string + if err = json.Unmarshal(body, &ids); err != nil { + return nil, err + } + return ids, nil } func (cl *APIClient) DownloadOriginal(file *File, outputPath string) error { @@ -425,24 +393,65 @@ func (cl *APIClient) DownloadOriginal(file *File, outputPath string) error { } func (cl *APIClient) jsonResponseOrThrow(resp *http.Response) ([]byte, error) { - if resp.Header.Get("Content-Type") == "application/json" { + if strings.HasPrefix(resp.Header.Get("content-type"), "application/json") { body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode > 299 { - var apiError APIErrorResponse - err = json.Unmarshal(body, &apiError) - if err != nil { + var apiError infra.APIErrorResponse + if err = json.Unmarshal(body, &apiError); err != nil { return nil, err } - return nil, &APIError{Value: apiError} + return nil, &infra.APIError{Value: apiError} + } else { + return body, nil } - return body, nil } else { - if resp.StatusCode > 299 { - return nil, errors.New(resp.Status) + return nil, errors.New("unexpected response format") + } +} + +func (cl *APIClient) successfulResponseOrThrow(resp *http.Response) error { + if resp.StatusCode > 299 { + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + var apiError infra.APIErrorResponse + if err = json.Unmarshal(body, &apiError); err != nil { + return err + } + return &infra.APIError{Value: apiError} + } else { + return nil + } +} + +type HealthAPIClient struct { + config *config.Config +} + +func NewHealthAPIClient() *HealthAPIClient { + return &HealthAPIClient{ + config: config.GetConfig(), + } +} + +func (cl *HealthAPIClient) GetHealth() (string, error) { + resp, err := http.Get(fmt.Sprintf("%s/v2/health", cl.config.IdPURL)) + if err != nil { + return "", err + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + infra.GetLogger().Error(err.Error()) } + }(resp.Body) + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err } - return nil, errors.New("unexpected response format") + return string(body), nil } diff --git a/webdav-go/client/idp_client.go b/webdav-go/client/idp_client.go index 27cba2974..89326efbd 100644 --- a/webdav-go/client/idp_client.go +++ b/webdav-go/client/idp_client.go @@ -3,37 +3,16 @@ package client import ( "bytes" "encoding/json" + "errors" "fmt" "io" "net/http" "net/url" + "strings" "voltaserve/config" "voltaserve/infra" ) -type IdPErrorResponse struct { - Code string `json:"code"` - Status int `json:"status"` - Message string `json:"message"` - UserMessage string `json:"userMessage"` - MoreInfo string `json:"moreInfo"` -} - -type IdPError struct { - Value IdPErrorResponse -} - -func (e *IdPError) Error() string { - return fmt.Sprintf("IdPError: %v", e.Value) -} - -type Token struct { - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` - TokenType string `json:"token_type"` - RefreshToken string `json:"refresh_token"` -} - const ( GrantTypePassword = "password" GrantTypeRefreshToken = "refresh_token" @@ -57,7 +36,7 @@ func NewIdPClient() *IdPClient { } } -func (cl *IdPClient) Exchange(options TokenExchangeOptions) (*Token, error) { +func (cl *IdPClient) Exchange(options TokenExchangeOptions) (*infra.Token, error) { form := url.Values{} form.Set("grant_type", options.GrantType) if options.Username != "" { @@ -85,38 +64,49 @@ func (cl *IdPClient) Exchange(options TokenExchangeOptions) (*Token, error) { infra.GetLogger().Error(err.Error()) } }(resp.Body) - return cl.jsonResponseOrThrow(resp) + body, err := cl.jsonResponseOrThrow(resp) + if err != nil { + return nil, err + } + var token infra.Token + if err = json.Unmarshal(body, &token); err != nil { + return nil, err + } + return &token, nil } -func (cl *IdPClient) jsonResponseOrThrow(resp *http.Response) (*Token, error) { - if resp.Header.Get("Content-Type") == "application/json" { - var jsonResponse Token +func (cl *IdPClient) jsonResponseOrThrow(resp *http.Response) ([]byte, error) { + if strings.HasPrefix(resp.Header.Get("content-type"), "application/json") { body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } - err = json.Unmarshal(body, &jsonResponse) - if err != nil { - return nil, err - } if resp.StatusCode > 299 { - var idpError IdPErrorResponse + var idpError infra.IdPErrorResponse err = json.Unmarshal(body, &idpError) if err != nil { return nil, err } - return nil, &IdPError{Value: idpError} + return nil, &infra.IdPError{Value: idpError} + } else { + return body, nil } - return &jsonResponse, nil } else { - if resp.StatusCode > 299 { - return nil, fmt.Errorf(resp.Status) - } + return nil, errors.New("unexpected response format") + } +} + +type HealthIdPClient struct { + config *config.Config +} + +func NewHealthIdPClient() *HealthIdPClient { + return &HealthIdPClient{ + config: config.GetConfig(), } - return nil, fmt.Errorf("unexpected response format") } -func (cl *IdPClient) GetHealth() (string, error) { +func (cl *HealthIdPClient) GetHealth() (string, error) { resp, err := http.Get(fmt.Sprintf("%s/v2/health", cl.config.IdPURL)) if err != nil { return "", err diff --git a/webdav-go/go.mod b/webdav-go/go.mod index 793e7686c..e14d72975 100644 --- a/webdav-go/go.mod +++ b/webdav-go/go.mod @@ -5,38 +5,13 @@ go 1.22 toolchain go1.22.2 require ( - github.com/gabriel-vasile/mimetype v1.4.4 - github.com/gofiber/fiber/v2 v2.52.5 github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 - github.com/minio/minio-go/v7 v7.0.72 github.com/speps/go-hashids/v2 v2.0.1 go.uber.org/zap v1.27.0 - gopkg.in/gographics/imagick.v3 v3.7.0 ) require ( - github.com/andybalholm/brotli v1.1.0 // indirect - github.com/disintegration/imaging v1.6.2 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/goccy/go-json v0.10.3 // indirect - github.com/klauspost/compress v1.17.9 // indirect - github.com/klauspost/cpuid/v2 v2.2.8 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/minio/md5-simd v1.1.2 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/rs/xid v1.5.0 // indirect github.com/stretchr/testify v1.9.0 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.55.0 // indirect - github.com/valyala/tcplisten v1.0.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.24.0 // indirect - golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect ) diff --git a/webdav-go/go.sum b/webdav-go/go.sum index ce4837c1b..52dadb21e 100644 --- a/webdav-go/go.sum +++ b/webdav-go/go.sum @@ -1,79 +1,20 @@ -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= -github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= -github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= -github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= -github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= -github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.72 h1:ZSbxs2BfJensLyHdVOgHv+pfmvxYraaUy07ER04dWnA= -github.com/minio/minio-go/v7 v7.0.72/go.mod h1:4yBA8v80xGA30cfM3fz0DKYMXunWl/AV/6tWEs9ryzo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/speps/go-hashids/v2 v2.0.1 h1:ViWOEqWES/pdOSq+C1SLVa8/Tnsd52XC34RY7lt7m4g= github.com/speps/go-hashids/v2 v2.0.1/go.mod h1:47LKunwvDZki/uRVD6NImtyk712yFzIs3UF3KlHohGw= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8= -github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= -github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= -golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -gopkg.in/gographics/imagick.v3 v3.7.0 h1:w8iQa58ikuqjX4l2OVML3pgqFcDMD8ywXJ9/cXa33fk= -gopkg.in/gographics/imagick.v3 v3.7.0/go.mod h1:+Q9nyA2xRZXrDyTtJ/eko+8V/5E7bWYs08ndkZp8UmA= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/webdav-go/handler/handler.go b/webdav-go/handler/handler.go index 5c3c5be05..8ab1db6f3 100644 --- a/webdav-go/handler/handler.go +++ b/webdav-go/handler/handler.go @@ -2,6 +2,7 @@ package handler import ( "net/http" + "voltaserve/client" ) type Handler struct{} @@ -37,6 +38,22 @@ func (h *Handler) Dispatch(w http.ResponseWriter, r *http.Request) { } } -func (h *Handler) Health(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) +func (h *Handler) Health(w http.ResponseWriter, _ *http.Request) { + apiClient := client.NewHealthAPIClient() + apiHealth, err := apiClient.GetHealth() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + idpClient := client.NewHealthIdPClient() + idpHealth, err := idpClient.GetHealth() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + if apiHealth == "OK" && idpHealth == "OK" { + w.WriteHeader(http.StatusOK) + return + } + w.WriteHeader(http.StatusServiceUnavailable) } diff --git a/webdav-go/handler/method_copy.go b/webdav-go/handler/method_copy.go index 89faf22c9..c9b90fc3b 100644 --- a/webdav-go/handler/method_copy.go +++ b/webdav-go/handler/method_copy.go @@ -1,9 +1,64 @@ package handler import ( + "fmt" "net/http" + "path" + "voltaserve/client" + "voltaserve/helper" + "voltaserve/infra" ) +/* +This method copies a resource from a source URL to a destination URL. + +Example implementation: + +- Extract the source and destination paths from the headers or request body. +- Use fs.copyFile() to copy the file from the source to the destination. +- Set the response status code to 204 if successful or an appropriate error code if the source file is not found or encountered an error. +- Return the response. +*/ func (h *Handler) methodCopy(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) + token, ok := r.Context().Value("token").(*infra.Token) + if !ok { + infra.HandleError(fmt.Errorf("missing token"), w) + return + } + apiClient := client.NewAPIClient(token) + sourcePath := helper.DecodeURIComponent(r.URL.Path) + targetPath := helper.DecodeURIComponent(helper.GetTargetPath(r)) + sourceFile, err := apiClient.GetFileByPath(sourcePath) + if err != nil { + infra.HandleError(err, w) + return + } + targetDir := helper.DecodeURIComponent(helper.Dirname(helper.GetTargetPath(r))) + targetFile, err := apiClient.GetFileByPath(targetDir) + if err != nil { + infra.HandleError(err, w) + return + } + if sourceFile.WorkspaceID != targetFile.WorkspaceID { + w.WriteHeader(http.StatusBadRequest) + if _, err := w.Write([]byte("Source and target files are in different workspaces")); err != nil { + return + } + } else { + clones, err := apiClient.CopyFile(targetFile.ID, client.FileCopyOptions{ + IDs: []string{sourceFile.ID}, + }) + if err != nil { + infra.HandleError(err, w) + return + } + _, err = apiClient.PatchFileName(clones[0].ID, client.FileRenameOptions{ + Name: path.Base(targetPath), + }) + if err != nil { + infra.HandleError(err, w) + return + } + w.WriteHeader(http.StatusNoContent) + } } diff --git a/webdav-go/handler/method_delete.go b/webdav-go/handler/method_delete.go index f76d3259d..863ce87ca 100644 --- a/webdav-go/handler/method_delete.go +++ b/webdav-go/handler/method_delete.go @@ -1,9 +1,38 @@ package handler import ( + "fmt" "net/http" + "voltaserve/client" + "voltaserve/helper" + "voltaserve/infra" ) +/* +This method deletes a resource identified by the URL. + +Example implementation: + +- Extract the file path from the URL. +- Use fs.unlink() to delete the file. +- Set the response status code to 204 if successful or an appropriate error code if the file is not found. +- Return the response. +*/ func (h *Handler) methodDelete(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) + token, ok := r.Context().Value("token").(*infra.Token) + if !ok { + infra.HandleError(fmt.Errorf("missing token"), w) + return + } + apiClient := client.NewAPIClient(token) + file, err := apiClient.GetFileByPath(helper.DecodeURIComponent(r.URL.Path)) + if err != nil { + infra.HandleError(err, w) + return + } + if _, err = apiClient.DeleteFile(file.ID); err != nil { + infra.HandleError(err, w) + return + } + w.WriteHeader(http.StatusNoContent) } diff --git a/webdav-go/handler/method_get.go b/webdav-go/handler/method_get.go index 866b306be..a5185d0c5 100644 --- a/webdav-go/handler/method_get.go +++ b/webdav-go/handler/method_get.go @@ -1,9 +1,115 @@ package handler import ( + "fmt" + "github.com/google/uuid" + "io" "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "voltaserve/client" + "voltaserve/helper" + "voltaserve/infra" ) +/* +This method retrieves the content of a resource identified by the URL. + +Example implementation: + +- Extract the file path from the URL. +- Create a read stream from the file and pipe it to the response stream. +- Set the response status code to 200 if successful or an appropriate error code if the file is not found. +- Return the response. +*/ func (h *Handler) methodGet(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) + token, ok := r.Context().Value("token").(*infra.Token) + if !ok { + infra.HandleError(fmt.Errorf("missing token"), w) + return + } + apiClient := client.NewAPIClient(token) + filePath := helper.DecodeURIComponent(r.URL.Path) + file, err := apiClient.GetFileByPath(filePath) + if err != nil { + infra.HandleError(err, w) + return + } + outputPath := filepath.Join(os.TempDir(), uuid.New().String()) + err = apiClient.DownloadOriginal(file, outputPath) + if err != nil { + infra.HandleError(err, w) + return + } + stat, err := os.Stat(outputPath) + if err != nil { + infra.HandleError(err, w) + return + } + rangeHeader := r.Header.Get("Range") + if rangeHeader != "" { + rangeHeader = strings.Replace(rangeHeader, "bytes=", "", 1) + parts := strings.Split(rangeHeader, "-") + rangeStart, err := strconv.ParseInt(parts[0], 10, 64) + if err != nil { + rangeStart = 0 + } + rangeEnd := stat.Size() - 1 + if len(parts) > 1 && parts[1] != "" { + rangeEnd, err = strconv.ParseInt(parts[1], 10, 64) + if err != nil { + rangeEnd = stat.Size() - 1 + } + } + chunkSize := rangeEnd - rangeStart + 1 + w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", rangeStart, rangeEnd, stat.Size())) + w.Header().Set("Accept-Ranges", "bytes") + w.Header().Set("Content-Length", fmt.Sprintf("%d", chunkSize)) + w.Header().Set("Content-Type", "application/octet-stream") + w.WriteHeader(http.StatusPartialContent) + file, err := os.Open(outputPath) + if err != nil { + infra.HandleError(err, w) + return + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + infra.HandleError(err, w) + } + }(file) + if _, err := file.Seek(rangeStart, 0); err != nil { + infra.HandleError(err, w) + return + } + if _, err := io.CopyN(w, file, chunkSize); err != nil { + return + } + if err := os.Remove(outputPath); err != nil { + return + } + } else { + w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size())) + w.Header().Set("Content-Type", "application/octet-stream") + w.WriteHeader(http.StatusOK) + file, err := os.Open(outputPath) + if err != nil { + infra.HandleError(err, w) + return + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + infra.HandleError(err, w) + } + }(file) + if _, err := io.Copy(w, file); err != nil { + return + } + if err := os.Remove(outputPath); err != nil { + return + } + } } diff --git a/webdav-go/handler/method_head.go b/webdav-go/handler/method_head.go index 418234ad9..bac971fc0 100644 --- a/webdav-go/handler/method_head.go +++ b/webdav-go/handler/method_head.go @@ -1,9 +1,39 @@ package handler import ( + "fmt" "net/http" + "voltaserve/client" + "voltaserve/helper" + "voltaserve/infra" ) +/* +This method is similar to GET but only retrieves the metadata of a resource, without returning the actual content. + +Example implementation: + +- Extract the file path from the URL. +- Retrieve the file metadata using fs.stat(). +- Set the response status code to 200 if successful or an appropriate error code if the file is not found. +- Set the Content-Length header with the file size. +- Return the response. +*/ func (h *Handler) methodHead(w http.ResponseWriter, r *http.Request) { + token, ok := r.Context().Value("token").(*infra.Token) + if !ok { + infra.HandleError(fmt.Errorf("missing token"), w) + return + } + apiClient := client.NewAPIClient(token) + filePath := helper.DecodeURIComponent(r.URL.Path) + file, err := apiClient.GetFileByPath(filePath) + if err != nil { + infra.HandleError(err, w) + return + } + if file.Type == client.FileTypeFile { + w.Header().Set("Content-Length", fmt.Sprintf("%d", file.Snapshot.Original.Size)) + } w.WriteHeader(http.StatusOK) } diff --git a/webdav-go/handler/method_mkcol.go b/webdav-go/handler/method_mkcol.go index 480cb898e..32abcf1d2 100644 --- a/webdav-go/handler/method_mkcol.go +++ b/webdav-go/handler/method_mkcol.go @@ -1,9 +1,46 @@ package handler import ( + "fmt" "net/http" + "path" + "voltaserve/client" + "voltaserve/helper" + "voltaserve/infra" ) +/* +This method creates a new collection (directory) at the specified URL. + +Example implementation: + +- Extract the directory path from the URL. +- Use fs.mkdir() to create the directory. +- Set the response status code to 201 if created or an appropriate error code if the directory already exists or encountered an error. +- Return the response. +*/ func (h *Handler) methodMkcol(w http.ResponseWriter, r *http.Request) { + token, ok := r.Context().Value("token").(*infra.Token) + if !ok { + infra.HandleError(fmt.Errorf("missing token"), w) + return + } + apiClient := client.NewAPIClient(token) + directoryPath := helper.DecodeURIComponent(helper.Dirname(r.URL.Path)) + directory, err := apiClient.GetFileByPath(directoryPath) + if err != nil { + infra.HandleError(err, w) + return + } + _, err = apiClient.CreateFile(client.FileCreateOptions{ + Type: client.FileTypeFolder, + WorkspaceID: directory.WorkspaceID, + ParentID: directory.ID, + Name: helper.DecodeURIComponent(path.Base(r.URL.Path)), + }) + if err != nil { + infra.HandleError(err, w) + return + } w.WriteHeader(http.StatusCreated) } diff --git a/webdav-go/handler/method_move.go b/webdav-go/handler/method_move.go index 124da0848..b9e1736d5 100644 --- a/webdav-go/handler/method_move.go +++ b/webdav-go/handler/method_move.go @@ -1,9 +1,69 @@ package handler import ( + "fmt" "net/http" + "path" + "strings" + "voltaserve/client" + "voltaserve/helper" + "voltaserve/infra" ) +/* +This method moves or renames a resource from a source URL to a destination URL. + +Example implementation: + +- Extract the source and destination paths from the headers or request body. +- Use fs.rename() to move or rename the file from the source to the destination. +- Set the response status code to 204 if successful or an appropriate error code if the source file is not found or encountered an error. +- Return the response. +*/ func (h *Handler) methodMove(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) + token, ok := r.Context().Value("token").(*infra.Token) + if !ok { + infra.HandleError(fmt.Errorf("missing token"), w) + return + } + apiClient := client.NewAPIClient(token) + sourcePath := helper.DecodeURIComponent(r.URL.Path) + targetPath := helper.DecodeURIComponent(helper.GetTargetPath(r)) + sourceFile, err := apiClient.GetFileByPath(sourcePath) + if err != nil { + infra.HandleError(err, w) + return + } + targetDir := helper.DecodeURIComponent(helper.Dirname(helper.GetTargetPath(r))) + targetFile, err := apiClient.GetFileByPath(targetDir) + if err != nil { + infra.HandleError(err, w) + return + } + if sourceFile.WorkspaceID != targetFile.WorkspaceID { + w.WriteHeader(http.StatusBadRequest) + if _, err := w.Write([]byte("Source and target files are in different workspaces")); err != nil { + infra.HandleError(err, w) + return + } + } else { + sourcePathParts := strings.Split(sourcePath, "/") + targetPathParts := strings.Split(targetPath, "/") + if len(sourcePathParts) == len(targetPathParts) && helper.Dirname(sourcePath) == helper.Dirname(targetPath) { + if _, err := apiClient.PatchFileName(sourceFile.ID, client.FileRenameOptions{ + Name: helper.DecodeURIComponent(path.Base(targetPath)), + }); err != nil { + infra.HandleError(err, w) + return + } + } else { + if err := apiClient.MoveFile(targetFile.ID, client.FileMoveOptions{ + IDs: []string{sourceFile.ID}, + }); err != nil { + infra.HandleError(err, w) + return + } + } + w.WriteHeader(http.StatusNoContent) + } } diff --git a/webdav-go/handler/method_options.go b/webdav-go/handler/method_options.go index 0389be26d..d4638c338 100644 --- a/webdav-go/handler/method_options.go +++ b/webdav-go/handler/method_options.go @@ -4,6 +4,16 @@ import ( "net/http" ) -func (h *Handler) methodOptions(w http.ResponseWriter, r *http.Request) { +/* +This method should respond with the allowed methods and capabilities of the server. + +Example implementation: + +- Set the response status code to 200. +- Set the Allow header to specify the supported methods, such as OPTIONS, GET, PUT, DELETE, etc. +- Return the response. +*/ +func (h *Handler) methodOptions(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Allow", "OPTIONS, GET, HEAD, PUT, DELETE, MKCOL, COPY, MOVE, PROPFIND, PROPPATCH") w.WriteHeader(http.StatusOK) } diff --git a/webdav-go/handler/method_propfind.go b/webdav-go/handler/method_propfind.go index 28328a0bc..bbaad2d91 100644 --- a/webdav-go/handler/method_propfind.go +++ b/webdav-go/handler/method_propfind.go @@ -1,9 +1,126 @@ package handler import ( + "fmt" "net/http" + "voltaserve/client" + "voltaserve/helper" + "voltaserve/infra" ) +/* +This method retrieves properties and metadata of a resource. + +Example implementation: + +- Extract the file path from the URL. +- Use fs.stat() to retrieve the file metadata. +- Format the response body in the desired XML format with the properties and metadata. +- Set the response status code to 207 if successful or an appropriate error code if the file is not found or encountered an error. +- Set the Content-Type header to indicate the XML format. +- Return the response. +*/ func (h *Handler) methodPropfind(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) + token, ok := r.Context().Value("token").(*infra.Token) + if !ok { + infra.HandleError(fmt.Errorf("missing token"), w) + return + } + apiClient := client.NewAPIClient(token) + file, err := apiClient.GetFileByPath(helper.DecodeURIComponent(r.URL.Path)) + if err != nil { + infra.HandleError(err, w) + return + } + if file.Type == client.FileTypeFile { + responseXml := fmt.Sprintf( + ` + + %s + + + + %d + %s + %s + + HTTP/1.1 200 OK + + + `, + helper.EncodeURIComponent(file.Name), + file.Snapshot.Original.Size, + helper.ToUTCString(&file.CreateTime), + helper.ToUTCString(file.UpdateTime), + ) + w.Header().Set("Content-Type", "application/xml; charset=utf-8") + w.WriteHeader(http.StatusMultiStatus) + if _, err := w.Write([]byte(responseXml)); err != nil { + infra.HandleError(err, w) + return + } + } else if file.Type == client.FileTypeFolder { + responseXml := fmt.Sprintf( + ` + + %s + + + + 0 + %s + %s + + HTTP/1.1 200 OK + + `, + helper.EncodeURIComponent(r.URL.Path), + helper.ToUTCString(file.UpdateTime), + helper.ToUTCString(&file.CreateTime), + ) + list, err := apiClient.ListFilesByPath(helper.DecodeURIComponent(r.URL.Path)) + if err != nil { + infra.HandleError(err, w) + return + } + for _, item := range list { + itemXml := fmt.Sprintf( + ` + %s + + + %s + %d + %s + %s + + HTTP/1.1 200 OK + + `, + helper.EncodeURIComponent(r.URL.Path+item.Name), + func() string { + if item.Type == client.FileTypeFolder { + return "" + } + return "" + }(), + func() int { + if item.Type == client.FileTypeFile && item.Snapshot.Original != nil { + return item.Snapshot.Original.Size + } + return 0 + }(), + helper.ToUTCString(item.UpdateTime), + helper.ToUTCString(&item.CreateTime), + ) + responseXml += itemXml + } + responseXml += `` + w.Header().Set("Content-Type", "application/xml; charset=utf-8") + w.WriteHeader(http.StatusMultiStatus) + if _, err := w.Write([]byte(responseXml)); err != nil { + infra.HandleError(err, w) + return + } + } } diff --git a/webdav-go/handler/method_proppatch.go b/webdav-go/handler/method_proppatch.go index 45e7271c7..46ef64ff3 100644 --- a/webdav-go/handler/method_proppatch.go +++ b/webdav-go/handler/method_proppatch.go @@ -4,6 +4,35 @@ import ( "net/http" ) -func (h *Handler) methodProppatch(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) +/* +This method updates the properties of a resource. + +Example implementation: + +- Parse the request body to extract the properties to be updated. +- Read the existing data from the file. +- Parse the existing properties. +- Merge the updated properties with the existing ones. +- Format the updated properties and store them back in the file. +- Set the response status code to 204 if successful or an appropriate error code if the file is not found or encountered an error. +- Return the response. + +In this example implementation, the handleProppatch() method first parses the XML +payload containing the properties to be updated. Then, it reads the existing data from the file, +parses the existing properties (assuming an XML format), +merges the updated properties with the existing ones, and formats +the properties back into the desired format (e.g., XML). + +Finally, the updated properties are written back to the file. +You can customize the parseProperties() and formatProperties() +functions to match the specific property format you are using in your WebDAV server. + +Note that this implementation assumes a simplified example and may require further +customization based on your specific property format and requirements. +*/ +func (h *Handler) methodProppatch(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotImplemented) + if _, err := w.Write([]byte(http.StatusText(http.StatusNotImplemented))); err != nil { + return + } } diff --git a/webdav-go/handler/method_put.go b/webdav-go/handler/method_put.go index 10558cc42..45c0b6076 100644 --- a/webdav-go/handler/method_put.go +++ b/webdav-go/handler/method_put.go @@ -1,9 +1,95 @@ package handler import ( + "fmt" + "github.com/google/uuid" + "io" "net/http" + "os" + "path" + "path/filepath" + "voltaserve/client" + "voltaserve/helper" + "voltaserve/infra" ) +/* +This method creates or updates a resource with the provided content. + +Example implementation: + +- Extract the file path from the URL. +- Create a write stream to the file. +- Listen for the data event to write the incoming data to the file. +- Listen for the end event to indicate the completion of the write stream. +- Set the response status code to 201 if created or 204 if updated. +- Return the response. +*/ func (h *Handler) methodPut(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) + token, ok := r.Context().Value("token").(*infra.Token) + if !ok { + infra.HandleError(fmt.Errorf("missing token"), w) + return + } + name := helper.DecodeURIComponent(path.Base(r.URL.Path)) + if helper.IsMicrosoftOfficeLockFile(name) || helper.IsOpenOfficeOfficeLockFile(name) { + w.WriteHeader(http.StatusOK) + return + } + apiClient := client.NewAPIClient(token) + directoryPath := helper.DecodeURIComponent(helper.Dirname(r.URL.Path)) + directory, err := apiClient.GetFileByPath(directoryPath) + if err != nil { + infra.HandleError(err, w) + return + } + outputPath := filepath.Join(os.TempDir(), uuid.New().String()) + ws, err := os.Create(outputPath) + if err != nil { + infra.HandleError(err, w) + return + } + defer ws.Close() + _, err = io.Copy(ws, r.Body) + if err != nil { + infra.HandleError(err, w) + return + } + err = ws.Close() + if err != nil { + infra.HandleError(err, w) + return + } + fileData, err := os.ReadFile(outputPath) + if err != nil { + infra.HandleError(err, w) + return + } + blob := fileData + existingFile, err := apiClient.GetFileByPath(r.URL.Path) + if err == nil { + _, err = apiClient.PatchFile(client.FilePatchOptions{ + ID: existingFile.ID, + Blob: blob, + Name: name, + }) + if err != nil { + infra.HandleError(err, w) + return + } + w.WriteHeader(http.StatusCreated) + return + } + _, err = apiClient.CreateFile(client.FileCreateOptions{ + Type: client.FileTypeFile, + WorkspaceID: directory.WorkspaceID, + ParentID: directory.ID, + Blob: blob, + Name: name, + }) + if err != nil { + infra.HandleError(err, w) + return + } + w.WriteHeader(http.StatusCreated) } diff --git a/webdav-go/helper/offlice-lock-files.go b/webdav-go/helper/offlice-lock-files.go new file mode 100644 index 000000000..4cedb7b41 --- /dev/null +++ b/webdav-go/helper/offlice-lock-files.go @@ -0,0 +1,11 @@ +package helper + +import "strings" + +func IsMicrosoftOfficeLockFile(name string) bool { + return strings.HasPrefix(name, "~$") +} + +func IsOpenOfficeOfficeLockFile(name string) bool { + return strings.HasPrefix(name, ".~lock.") && strings.HasSuffix(name, "#") +} diff --git a/webdav-go/helper/path.go b/webdav-go/helper/path.go new file mode 100644 index 000000000..5a9fc7e1b --- /dev/null +++ b/webdav-go/helper/path.go @@ -0,0 +1,34 @@ +package helper + +import ( + "net/http" + "net/url" + "path" + "strings" +) + +func GetTargetPath(req *http.Request) string { + destination := req.Header.Get("Destination") + if destination == "" { + return "" + } + /* Check if the destination header is a full URL */ + if strings.HasPrefix(destination, "http://") || strings.HasPrefix(destination, "https://") { + parsedURL, err := url.Parse(destination) + if err != nil { + return "" + } + return parsedURL.Path + } + /* Extract the path from the destination header */ + startIndex := strings.Index(destination, req.Host) + len(req.Host) + if startIndex < len(req.Host) { + return "" + } + return destination[startIndex:] +} + +func Dirname(value string) string { + trimmedValue := strings.TrimSuffix(value, "/") + return path.Dir(trimmedValue) +} diff --git a/webdav-go/helper/pointer.go b/webdav-go/helper/pointer.go deleted file mode 100644 index 6fce5daff..000000000 --- a/webdav-go/helper/pointer.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2023 Anass Bouassaba. -// -// Use of this software is governed by the Business Source License -// included in the file licenses/BSL.txt. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the GNU Affero General Public License v3.0 only, included in the file -// licenses/AGPL.txt. - -package helper - -func ToPtr[T any](v T) *T { - return &v -} diff --git a/webdav-go/helper/time.go b/webdav-go/helper/time.go new file mode 100644 index 000000000..82dcbd224 --- /dev/null +++ b/webdav-go/helper/time.go @@ -0,0 +1,14 @@ +package helper + +import "time" + +func ToUTCString(value *string) string { + if value == nil { + return "" + } + parsedTime, err := time.Parse(time.RFC3339, *value) + if err != nil { + return "" + } + return parsedTime.Format(time.RFC1123) +} diff --git a/webdav-go/helper/token.go b/webdav-go/helper/token.go new file mode 100644 index 000000000..1ec959f6a --- /dev/null +++ b/webdav-go/helper/token.go @@ -0,0 +1,10 @@ +package helper + +import ( + "time" + "voltaserve/infra" +) + +func NewExpiry(token *infra.Token) time.Time { + return time.Now().Add(time.Duration(token.ExpiresIn) * time.Second) +} diff --git a/webdav-go/helper/uri.go b/webdav-go/helper/uri.go new file mode 100644 index 000000000..46486763b --- /dev/null +++ b/webdav-go/helper/uri.go @@ -0,0 +1,21 @@ +package helper + +import ( + "net/url" + "strings" +) + +func DecodeURIComponent(value string) string { + res, err := url.PathUnescape(value) + if err != nil { + return "" + } + return res +} + +func EncodeURIComponent(value string) string { + encoded := url.QueryEscape(value) + encoded = strings.ReplaceAll(encoded, "%2F", "/") + encoded = strings.ReplaceAll(encoded, "+", "%20") + return encoded +} diff --git a/webdav-go/infra/error.go b/webdav-go/infra/error.go new file mode 100644 index 000000000..af5ea4f02 --- /dev/null +++ b/webdav-go/infra/error.go @@ -0,0 +1,65 @@ +package infra + +import ( + "errors" + "fmt" + "log" + "net/http" +) + +type IdPErrorResponse struct { + Code string `json:"code"` + Status int `json:"status"` + Message string `json:"message"` + UserMessage string `json:"userMessage"` + MoreInfo string `json:"moreInfo"` +} + +type IdPError struct { + Value IdPErrorResponse +} + +func (e *IdPError) Error() string { + return fmt.Sprintf("IdPError: %v", e.Value) +} + +type APIErrorResponse struct { + Code string `json:"code"` + Status int `json:"status"` + Message string `json:"message"` + UserMessage string `json:"userMessage"` + MoreInfo string `json:"moreInfo"` +} + +type APIError struct { + Value APIErrorResponse +} + +func (e *APIError) Error() string { + return fmt.Sprintf("APIError: %v", e.Value) +} + +func HandleError(err error, w http.ResponseWriter) { + var apiErr *APIError + var idpErr *IdPError + switch { + case errors.As(err, &apiErr): + w.WriteHeader(apiErr.Value.Status) + if _, err := w.Write([]byte(apiErr.Value.UserMessage)); err != nil { + GetLogger().Error(err) + return + } + case errors.As(err, &idpErr): + w.WriteHeader(idpErr.Value.Status) + if _, err := w.Write([]byte(idpErr.Value.UserMessage)); err != nil { + GetLogger().Error(err) + return + } + default: + w.WriteHeader(http.StatusInternalServerError) + if _, err := w.Write([]byte("Internal Server Error")); err != nil { + return + } + } + log.Println(err) +} diff --git a/webdav-go/infra/token.go b/webdav-go/infra/token.go new file mode 100644 index 000000000..cdb09ce8e --- /dev/null +++ b/webdav-go/infra/token.go @@ -0,0 +1,8 @@ +package infra + +type Token struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + TokenType string `json:"token_type"` + RefreshToken string `json:"refresh_token"` +} diff --git a/webdav-go/main.go b/webdav-go/main.go index 2fa223ab5..4df25d3f0 100644 --- a/webdav-go/main.go +++ b/webdav-go/main.go @@ -17,47 +17,47 @@ import ( "net/http" "os" "strings" - "sync" "time" "voltaserve/client" "voltaserve/handler" + "voltaserve/helper" + "voltaserve/infra" "github.com/joho/godotenv" "voltaserve/config" ) var ( - tokens = make(map[string]*client.Token) + tokens = make(map[string]*infra.Token) expiries = make(map[string]time.Time) - api = &client.IdPClient{} - mu sync.Mutex + //mu sync.Mutex ) -func startTokenRefresh() { +func startTokenRefresh(idpClient *client.IdPClient) { ticker := time.NewTicker(5 * time.Second) go func() { for { <-ticker.C - mu.Lock() + //mu.Lock() for username, token := range tokens { expiry := expiries[username] if time.Now().After(expiry.Add(-1 * time.Minute)) { - newToken, err := api.Exchange(client.TokenExchangeOptions{ + newToken, err := idpClient.Exchange(client.TokenExchangeOptions{ GrantType: client.GrantTypeRefreshToken, RefreshToken: token.RefreshToken, }) if err == nil { tokens[username] = newToken - expiries[username] = time.Now().Add(time.Duration(newToken.ExpiresIn) * time.Second) + expiries[username] = helper.NewExpiry(newToken) } } } - mu.Unlock() + //mu.Unlock() } }() } -func basicAuthMiddleware(next http.Handler) http.Handler { +func basicAuthMiddleware(next http.Handler, idpClient *client.IdPClient) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { username, password, ok := r.BasicAuth() if !ok { @@ -65,11 +65,12 @@ func basicAuthMiddleware(next http.Handler) http.Handler { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } - mu.Lock() - defer mu.Unlock() + //mu.Lock() + //defer mu.Unlock() token, exists := tokens[username] if !exists { - token, err := api.Exchange(client.TokenExchangeOptions{ + var err error + token, err = idpClient.Exchange(client.TokenExchangeOptions{ GrantType: client.GrantTypePassword, Username: username, Password: password, @@ -81,8 +82,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler { tokens[username] = token expiries[username] = time.Now().Add(time.Duration(token.ExpiresIn) * time.Second) } - ctx := context.WithValue(r.Context(), "token", token) - next.ServeHTTP(w, r.WithContext(ctx)) + next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "token", token))) }) } @@ -91,32 +91,32 @@ func basicAuthMiddleware(next http.Handler) http.Handler { // @BasePath /v2 func main() { if _, err := os.Stat(".env.local"); err == nil { - err := godotenv.Load(".env.local") - if err != nil { + if err := godotenv.Load(".env.local"); err != nil { panic(err) } } else { - err := godotenv.Load() - if err != nil { + if err := godotenv.Load(); err != nil { panic(err) } } cfg := config.GetConfig() + idpClient := client.NewIdPClient() + h := handler.NewHandler() mux := http.NewServeMux() mux.HandleFunc("/v2/health", h.Health) mux.HandleFunc("/", h.Dispatch) - startTokenRefresh() + startTokenRefresh(idpClient) log.Printf("Listening on port %d", cfg.Port) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", cfg.Port), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/v2/health") { mux.ServeHTTP(w, r) } else { - basicAuthMiddleware(mux).ServeHTTP(w, r) + basicAuthMiddleware(mux, idpClient).ServeHTTP(w, r) } }))) } diff --git a/webdav/package.json b/webdav/package.json index f430d504c..32e3536f4 100644 --- a/webdav/package.json +++ b/webdav/package.json @@ -6,6 +6,8 @@ "scripts": { "start": "bun src/server.ts", "dev": "bun dev src/server.ts", + "start:node": "ts-node -r tsconfig-paths/register src/server.ts", + "dev:node": "nodemon --exec ts-node -r tsconfig-paths/register src/server.ts", "tsc": "tsc --noEmit", "format": "prettier --write .", "lint": "eslint" diff --git a/webdav/src/server.ts b/webdav/src/server.ts index 3027291c8..f55d4ca91 100644 --- a/webdav/src/server.ts +++ b/webdav/src/server.ts @@ -80,6 +80,7 @@ function handleRequest(req: IncomingMessage, res: ServerResponse) { res.end() } else { const method = req.method.toUpperCase() + console.log(method) switch (method) { case 'OPTIONS': await handleOptions(req, res)