mirror of
https://github.com/get-drexa/drive.git
synced 2026-02-02 12:01:17 +00:00
docs: add OpenAPI documentation with Scalar UI
- Add swaggo annotations to all HTTP handlers - Add Swagger/OpenAPI spec generation with swag - Create separate docs server binary (drexa-docs) - Add Makefile with build, run, and docs targets - Configure Scalar as the API documentation UI Run 'make docs' to regenerate, 'make run-docs' to serve.
This commit is contained in:
41
apps/backend/Makefile
Normal file
41
apps/backend/Makefile
Normal file
@@ -0,0 +1,41 @@
|
||||
.PHONY: build build-docs build-all run run-docs docs install-tools fmt clean
|
||||
|
||||
# Build the API server
|
||||
build:
|
||||
go build -o bin/drexa ./cmd/drexa
|
||||
|
||||
# Build the documentation server
|
||||
build-docs:
|
||||
go build -o bin/drexa-docs ./cmd/docs
|
||||
|
||||
# Build all binaries
|
||||
build-all: build build-docs
|
||||
|
||||
# Run the API server
|
||||
run:
|
||||
go run ./cmd/drexa --config config.yaml
|
||||
|
||||
# Run the documentation server
|
||||
run-docs:
|
||||
go run ./cmd/docs --port 8081 --api-url http://localhost:8080
|
||||
|
||||
# Generate API documentation
|
||||
docs:
|
||||
@echo "Generating OpenAPI documentation..."
|
||||
swag init -g cmd/drexa/main.go -o docs --parseDependency --parseInternal --outputTypes go,json,yaml
|
||||
@echo "Documentation generated in docs/"
|
||||
@echo "Run 'make run-docs' to start the documentation server"
|
||||
|
||||
# Install development tools
|
||||
install-tools:
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
|
||||
# Format and lint
|
||||
fmt:
|
||||
go fmt ./...
|
||||
swag fmt
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
rm -rf bin/
|
||||
rm -f docs/swagger.json docs/swagger.yaml
|
||||
84
apps/backend/cmd/docs/main.go
Normal file
84
apps/backend/cmd/docs/main.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
_ "github.com/get-drexa/drexa/docs"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/swaggo/swag"
|
||||
)
|
||||
|
||||
func main() {
|
||||
port := flag.Int("port", 8081, "port to listen on")
|
||||
apiURL := flag.String("api-url", "http://localhost:8080", "base URL of the API server")
|
||||
flag.Parse()
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
AppName: "Drexa API Documentation",
|
||||
})
|
||||
|
||||
app.Use(logger.New())
|
||||
app.Use(cors.New())
|
||||
|
||||
// Serve Scalar UI
|
||||
app.Get("/", func(c *fiber.Ctx) error {
|
||||
html := fmt.Sprintf(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Drexa API Documentation</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%%3E%%3Ctext y='.9em' font-size='90'%%3E📚%%3C/text%%3E%%3C/svg%%3E" />
|
||||
</head>
|
||||
<body>
|
||||
<script
|
||||
id="api-reference"
|
||||
data-url="/openapi.json"
|
||||
data-configuration='{
|
||||
"theme": "kepler",
|
||||
"darkMode": true,
|
||||
"authentication": {
|
||||
"preferredSecurityScheme": "BearerAuth"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "%s",
|
||||
"description": "API Server"
|
||||
}
|
||||
]
|
||||
}'
|
||||
></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
||||
</body>
|
||||
</html>`, *apiURL)
|
||||
c.Set("Content-Type", "text/html; charset=utf-8")
|
||||
return c.SendString(html)
|
||||
})
|
||||
|
||||
// Serve OpenAPI spec
|
||||
app.Get("/openapi.json", func(c *fiber.Ctx) error {
|
||||
c.Set("Content-Type", "application/json")
|
||||
c.Set("Access-Control-Allow-Origin", "*")
|
||||
doc, err := swag.ReadDoc()
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||
}
|
||||
return c.SendString(doc)
|
||||
})
|
||||
|
||||
// Health check
|
||||
app.Get("/health", func(c *fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{"status": "ok"})
|
||||
})
|
||||
|
||||
fmt.Fprintf(os.Stderr, "📚 Drexa API Documentation server starting on http://localhost:%d\n", *port)
|
||||
fmt.Fprintf(os.Stderr, " API server configured at: %s\n", *apiURL)
|
||||
|
||||
log.Fatal(app.Listen(fmt.Sprintf(":%d", *port)))
|
||||
}
|
||||
|
||||
@@ -6,9 +6,28 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
_ "github.com/get-drexa/drexa/docs"
|
||||
"github.com/get-drexa/drexa/internal/drexa"
|
||||
)
|
||||
|
||||
// @title Drexa API
|
||||
// @version 1.0
|
||||
// @description Drexa is a file storage and management API. It provides endpoints for authentication, user management, file uploads, and virtual filesystem operations.
|
||||
|
||||
// @contact.name Drexa Support
|
||||
// @contact.url https://github.com/get-drexa/drexa
|
||||
|
||||
// @license.name MIT
|
||||
// @license.url https://opensource.org/licenses/MIT
|
||||
|
||||
// @host localhost:8080
|
||||
// @BasePath /api
|
||||
|
||||
// @securityDefinitions.apikey BearerAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description JWT access token. Format: "Bearer {token}"
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "", "path to config file (required)")
|
||||
flag.Parse()
|
||||
|
||||
1601
apps/backend/docs/docs.go
Normal file
1601
apps/backend/docs/docs.go
Normal file
File diff suppressed because it is too large
Load Diff
1577
apps/backend/docs/swagger.json
Normal file
1577
apps/backend/docs/swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
1078
apps/backend/docs/swagger.yaml
Normal file
1078
apps/backend/docs/swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ require (
|
||||
github.com/gofiber/fiber/v2 v2.52.9
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/sqids/sqids-go v0.4.1
|
||||
github.com/swaggo/swag v1.16.6
|
||||
github.com/uptrace/bun v1.2.16
|
||||
github.com/uptrace/bun/extra/bundebug v1.2.16
|
||||
golang.org/x/crypto v0.45.0
|
||||
@@ -14,9 +15,24 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
mellium.im/sasl v0.3.2 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
|
||||
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
@@ -16,10 +34,19 @@ 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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
@@ -36,8 +63,13 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/sqids/sqids-go v0.4.1 h1:eQKYzmAZbLlRwHeHYPF35QhgxwZHLnlmVj9AkIj/rrw=
|
||||
github.com/sqids/sqids-go v0.4.1/go.mod h1:EMwHuPQgSNFS0A49jESTfIQS+066XQTVhukrzEPScl8=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
|
||||
github.com/uptrace/bun v1.2.16 h1:QlObi6ZIK5Ao7kAALnh91HWYNZUBbVwye52fmlQM9kc=
|
||||
@@ -64,12 +96,35 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0=
|
||||
|
||||
@@ -7,15 +7,23 @@ import (
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
// Account represents a storage account with quota information
|
||||
// @Description Storage account with usage and quota details
|
||||
type Account struct {
|
||||
bun.BaseModel `bun:"accounts"`
|
||||
bun.BaseModel `bun:"accounts" swaggerignore:"true"`
|
||||
|
||||
ID uuid.UUID `bun:",pk,type:uuid" json:"id"`
|
||||
UserID uuid.UUID `bun:"user_id,notnull,type:uuid" json:"userId"`
|
||||
StorageUsageBytes int64 `bun:"storage_usage_bytes,notnull" json:"storageUsageBytes"`
|
||||
StorageQuotaBytes int64 `bun:"storage_quota_bytes,notnull" json:"storageQuotaBytes"`
|
||||
CreatedAt time.Time `bun:"created_at,notnull,nullzero" json:"createdAt"`
|
||||
UpdatedAt time.Time `bun:"updated_at,notnull,nullzero" json:"updatedAt"`
|
||||
// Unique account identifier
|
||||
ID uuid.UUID `bun:",pk,type:uuid" json:"id" example:"550e8400-e29b-41d4-a716-446655440000"`
|
||||
// ID of the user who owns this account
|
||||
UserID uuid.UUID `bun:"user_id,notnull,type:uuid" json:"userId" example:"550e8400-e29b-41d4-a716-446655440001"`
|
||||
// Current storage usage in bytes
|
||||
StorageUsageBytes int64 `bun:"storage_usage_bytes,notnull" json:"storageUsageBytes" example:"1073741824"`
|
||||
// Maximum storage quota in bytes
|
||||
StorageQuotaBytes int64 `bun:"storage_quota_bytes,notnull" json:"storageQuotaBytes" example:"10737418240"`
|
||||
// When the account was created (ISO 8601)
|
||||
CreatedAt time.Time `bun:"created_at,notnull,nullzero" json:"createdAt" example:"2024-12-13T15:04:05Z"`
|
||||
// When the account was last updated (ISO 8601)
|
||||
UpdatedAt time.Time `bun:"updated_at,notnull,nullzero" json:"updatedAt" example:"2024-12-13T16:30:00Z"`
|
||||
}
|
||||
|
||||
func newAccountID() (uuid.UUID, error) {
|
||||
|
||||
@@ -19,17 +19,28 @@ type HTTPHandler struct {
|
||||
authMiddleware fiber.Handler
|
||||
}
|
||||
|
||||
// registerAccountRequest represents a new account registration
|
||||
// @Description Request to create a new account and user
|
||||
type registerAccountRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
DisplayName string `json:"displayName"`
|
||||
// Email address for the new account
|
||||
Email string `json:"email" example:"newuser@example.com"`
|
||||
// Password for the new account (min 8 characters)
|
||||
Password string `json:"password" example:"securepassword123"`
|
||||
// Display name for the user
|
||||
DisplayName string `json:"displayName" example:"Jane Doe"`
|
||||
}
|
||||
|
||||
// registerAccountResponse represents a successful registration
|
||||
// @Description Response after successful account registration
|
||||
type registerAccountResponse struct {
|
||||
// The created account
|
||||
Account *Account `json:"account"`
|
||||
// The created user
|
||||
User *user.User `json:"user"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
// JWT access token for immediate authentication
|
||||
AccessToken string `json:"accessToken" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAifQ.signature"`
|
||||
// Base64 URL encoded refresh token
|
||||
RefreshToken string `json:"refreshToken" example:"dR4nD0mUu1DkZXlCeXRlc0FuZFJhbmRvbURhdGFIZXJlMTIzNDU2Nzg5MGFi"`
|
||||
}
|
||||
|
||||
const currentAccountKey = "currentAccount"
|
||||
@@ -75,6 +86,17 @@ func (h *HTTPHandler) accountMiddleware(c *fiber.Ctx) error {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// getAccount retrieves account information
|
||||
// @Summary Get account
|
||||
// @Description Retrieve account details including storage usage and quota
|
||||
// @Tags accounts
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param accountID path string true "Account ID" format(uuid)
|
||||
// @Success 200 {object} Account "Account details"
|
||||
// @Failure 401 {string} string "Not authenticated"
|
||||
// @Failure 404 {string} string "Account not found"
|
||||
// @Router /accounts/{accountID} [get]
|
||||
func (h *HTTPHandler) getAccount(c *fiber.Ctx) error {
|
||||
account := CurrentAccount(c)
|
||||
if account == nil {
|
||||
@@ -83,6 +105,17 @@ func (h *HTTPHandler) getAccount(c *fiber.Ctx) error {
|
||||
return c.JSON(account)
|
||||
}
|
||||
|
||||
// registerAccount creates a new account and user
|
||||
// @Summary Register new account
|
||||
// @Description Create a new user account with email and password. Returns the account, user, and authentication tokens.
|
||||
// @Tags accounts
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body registerAccountRequest true "Registration details"
|
||||
// @Success 200 {object} registerAccountResponse "Account created successfully"
|
||||
// @Failure 400 {string} string "Invalid request body"
|
||||
// @Failure 409 {string} string "Email already registered"
|
||||
// @Router /accounts [post]
|
||||
func (h *HTTPHandler) registerAccount(c *fiber.Ctx) error {
|
||||
req := new(registerAccountRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
|
||||
@@ -20,25 +20,42 @@ const (
|
||||
cookieKeyRefreshToken = "refresh_token"
|
||||
)
|
||||
|
||||
// loginRequest represents the login credentials
|
||||
// @Description Login request with email, password, and token delivery preference
|
||||
type loginRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
TokenDelivery string `json:"tokenDelivery"`
|
||||
// User's email address
|
||||
Email string `json:"email" example:"user@example.com"`
|
||||
// User's password
|
||||
Password string `json:"password" example:"secretpassword123"`
|
||||
// How to deliver tokens: "cookie" (set HTTP-only cookies) or "body" (include in response)
|
||||
TokenDelivery string `json:"tokenDelivery" example:"body" enums:"cookie,body"`
|
||||
}
|
||||
|
||||
// loginResponse represents a successful login response
|
||||
// @Description Login response containing user info and optionally tokens
|
||||
type loginResponse struct {
|
||||
// Authenticated user information
|
||||
User user.User `json:"user"`
|
||||
AccessToken string `json:"accessToken,omitempty"`
|
||||
RefreshToken string `json:"refreshToken,omitempty"`
|
||||
// JWT access token (only included when tokenDelivery is "body")
|
||||
AccessToken string `json:"accessToken,omitempty" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAifQ.signature"`
|
||||
// Base64 URL encoded refresh token (only included when tokenDelivery is "body")
|
||||
RefreshToken string `json:"refreshToken,omitempty" example:"dR4nD0mUu1DkZXlCeXRlc0FuZFJhbmRvbURhdGFIZXJlMTIzNDU2Nzg5MGFi"`
|
||||
}
|
||||
|
||||
// refreshAccessTokenRequest represents a token refresh request
|
||||
// @Description Request to exchange a refresh token for new tokens
|
||||
type refreshAccessTokenRequest struct {
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
// Base64 URL encoded refresh token
|
||||
RefreshToken string `json:"refreshToken" example:"dR4nD0mUu1DkZXlCeXRlc0FuZFJhbmRvbURhdGFIZXJlMTIzNDU2Nzg5MGFi"`
|
||||
}
|
||||
|
||||
// tokenResponse represents new access and refresh tokens
|
||||
// @Description Response containing new access token and refresh token
|
||||
type tokenResponse struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
// New JWT access token
|
||||
AccessToken string `json:"accessToken" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAifQ.signature"`
|
||||
// New base64 URL encoded refresh token
|
||||
RefreshToken string `json:"refreshToken" example:"xK9mPqRsTuVwXyZ0AbCdEfGhIjKlMnOpQrStUvWxYz1234567890abcdefgh"`
|
||||
}
|
||||
|
||||
type HTTPHandler struct {
|
||||
@@ -57,6 +74,17 @@ func (h *HTTPHandler) RegisterRoutes(api fiber.Router) {
|
||||
auth.Post("/tokens", h.refreshAccessToken)
|
||||
}
|
||||
|
||||
// Login authenticates a user with email and password
|
||||
// @Summary User login
|
||||
// @Description Authenticate with email and password to receive JWT tokens. Tokens can be delivered via HTTP-only cookies or in the response body based on the tokenDelivery field.
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body loginRequest true "Login credentials"
|
||||
// @Success 200 {object} loginResponse "Successful authentication"
|
||||
// @Failure 400 {object} map[string]string "Invalid request body or token delivery method"
|
||||
// @Failure 401 {object} map[string]string "Invalid email or password"
|
||||
// @Router /auth/login [post]
|
||||
func (h *HTTPHandler) Login(c *fiber.Ctx) error {
|
||||
req := new(loginRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
@@ -100,6 +128,17 @@ func (h *HTTPHandler) Login(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
// refreshAccessToken exchanges a refresh token for new access and refresh tokens
|
||||
// @Summary Refresh access token
|
||||
// @Description Exchange a valid refresh token for a new pair of access and refresh tokens. The old refresh token is invalidated (rotation).
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body refreshAccessTokenRequest true "Refresh token"
|
||||
// @Success 200 {object} tokenResponse "New tokens"
|
||||
// @Failure 400 {object} map[string]string "Invalid request body"
|
||||
// @Failure 401 {object} map[string]string "Invalid, expired, or reused refresh token"
|
||||
// @Router /auth/tokens [post]
|
||||
func (h *HTTPHandler) refreshAccessToken(c *fiber.Ctx) error {
|
||||
req := new(refreshAccessTokenRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
|
||||
@@ -17,23 +17,39 @@ const (
|
||||
DirItemKindFile = "file"
|
||||
)
|
||||
|
||||
// DirectoryInfo represents directory metadata
|
||||
// @Description Directory information including path and timestamps
|
||||
type DirectoryInfo struct {
|
||||
Kind string `json:"kind"`
|
||||
ID string `json:"id"`
|
||||
// Item type, always "directory"
|
||||
Kind string `json:"kind" example:"directory"`
|
||||
// Unique directory identifier
|
||||
ID string `json:"id" example:"kRp2XYTq9A55"`
|
||||
// Full path from root (included when ?include=path)
|
||||
Path virtualfs.Path `json:"path,omitempty"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
DeletedAt *time.Time `json:"deletedAt,omitempty"`
|
||||
// Directory name
|
||||
Name string `json:"name" example:"My Documents"`
|
||||
// When the directory was created (ISO 8601)
|
||||
CreatedAt time.Time `json:"createdAt" example:"2024-12-13T15:04:05Z"`
|
||||
// When the directory was last updated (ISO 8601)
|
||||
UpdatedAt time.Time `json:"updatedAt" example:"2024-12-13T16:30:00Z"`
|
||||
// When the directory was trashed, null if not trashed (ISO 8601)
|
||||
DeletedAt *time.Time `json:"deletedAt,omitempty" example:"2024-12-14T10:00:00Z"`
|
||||
}
|
||||
|
||||
// createDirectoryRequest represents a new directory creation request
|
||||
// @Description Request to create a new directory
|
||||
type createDirectoryRequest struct {
|
||||
ParentID string `json:"parentID"`
|
||||
Name string `json:"name"`
|
||||
// ID of the parent directory
|
||||
ParentID string `json:"parentID" example:"kRp2XYTq9A55"`
|
||||
// Name for the new directory
|
||||
Name string `json:"name" example:"New Folder"`
|
||||
}
|
||||
|
||||
// postDirectoryContentRequest represents a move items request
|
||||
// @Description Request to move items into this directory
|
||||
type postDirectoryContentRequest struct {
|
||||
Items []string `json:"items"`
|
||||
// Array of file/directory IDs to move
|
||||
Items []string `json:"items" example:"mElnUNCm8F22,kRp2XYTq9A55"`
|
||||
}
|
||||
|
||||
func (h *HTTPHandler) currentDirectoryMiddleware(c *fiber.Ctx) error {
|
||||
@@ -64,6 +80,21 @@ func includeParam(c *fiber.Ctx) []string {
|
||||
return strings.Split(c.Query("include"), ",")
|
||||
}
|
||||
|
||||
// createDirectory creates a new directory
|
||||
// @Summary Create directory
|
||||
// @Description Create a new directory within a parent directory
|
||||
// @Tags directories
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param accountID path string true "Account ID" format(uuid)
|
||||
// @Param request body createDirectoryRequest true "Directory details"
|
||||
// @Param include query string false "Include additional fields" Enums(path)
|
||||
// @Success 200 {object} DirectoryInfo "Created directory"
|
||||
// @Failure 400 {object} map[string]string "Parent not found or not a directory"
|
||||
// @Failure 401 {string} string "Not authenticated"
|
||||
// @Failure 409 {object} map[string]string "Directory already exists"
|
||||
// @Router /accounts/{accountID}/directories [post]
|
||||
func (h *HTTPHandler) createDirectory(c *fiber.Ctx) error {
|
||||
account := account.CurrentAccount(c)
|
||||
if account == nil {
|
||||
@@ -127,6 +158,19 @@ func (h *HTTPHandler) createDirectory(c *fiber.Ctx) error {
|
||||
return c.JSON(i)
|
||||
}
|
||||
|
||||
// fetchDirectory returns directory metadata
|
||||
// @Summary Get directory info
|
||||
// @Description Retrieve metadata for a specific directory
|
||||
// @Tags directories
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param accountID path string true "Account ID" format(uuid)
|
||||
// @Param directoryID path string true "Directory ID"
|
||||
// @Param include query string false "Include additional fields" Enums(path)
|
||||
// @Success 200 {object} DirectoryInfo "Directory metadata"
|
||||
// @Failure 401 {string} string "Not authenticated"
|
||||
// @Failure 404 {string} string "Directory not found"
|
||||
// @Router /accounts/{accountID}/directories/{directoryID} [get]
|
||||
func (h *HTTPHandler) fetchDirectory(c *fiber.Ctx) error {
|
||||
node := mustCurrentDirectoryNode(c)
|
||||
|
||||
@@ -151,6 +195,18 @@ func (h *HTTPHandler) fetchDirectory(c *fiber.Ctx) error {
|
||||
return c.JSON(i)
|
||||
}
|
||||
|
||||
// listDirectory returns directory contents
|
||||
// @Summary List directory contents
|
||||
// @Description Get all files and subdirectories within a directory
|
||||
// @Tags directories
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param accountID path string true "Account ID" format(uuid)
|
||||
// @Param directoryID path string true "Directory ID"
|
||||
// @Success 200 {array} interface{} "Array of FileInfo and DirectoryInfo objects"
|
||||
// @Failure 401 {string} string "Not authenticated"
|
||||
// @Failure 404 {string} string "Directory not found"
|
||||
// @Router /accounts/{accountID}/directories/{directoryID}/content [get]
|
||||
func (h *HTTPHandler) listDirectory(c *fiber.Ctx) error {
|
||||
node := mustCurrentDirectoryNode(c)
|
||||
children, err := h.vfs.ListChildren(c.Context(), h.db, node)
|
||||
@@ -190,6 +246,21 @@ func (h *HTTPHandler) listDirectory(c *fiber.Ctx) error {
|
||||
return c.JSON(items)
|
||||
}
|
||||
|
||||
// patchDirectory updates directory properties
|
||||
// @Summary Update directory
|
||||
// @Description Update directory properties such as name (rename)
|
||||
// @Tags directories
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param accountID path string true "Account ID" format(uuid)
|
||||
// @Param directoryID path string true "Directory ID"
|
||||
// @Param request body patchDirectoryRequest true "Directory update"
|
||||
// @Success 200 {object} DirectoryInfo "Updated directory metadata"
|
||||
// @Failure 400 {object} map[string]string "Invalid request"
|
||||
// @Failure 401 {string} string "Not authenticated"
|
||||
// @Failure 404 {string} string "Directory not found"
|
||||
// @Router /accounts/{accountID}/directories/{directoryID} [patch]
|
||||
func (h *HTTPHandler) patchDirectory(c *fiber.Ctx) error {
|
||||
node := mustCurrentDirectoryNode(c)
|
||||
|
||||
@@ -229,6 +300,18 @@ func (h *HTTPHandler) patchDirectory(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
// deleteDirectory removes a directory
|
||||
// @Summary Delete directory
|
||||
// @Description Delete a directory permanently or move it to trash. Deleting a directory also affects all its contents.
|
||||
// @Tags directories
|
||||
// @Security BearerAuth
|
||||
// @Param accountID path string true "Account ID" format(uuid)
|
||||
// @Param directoryID path string true "Directory ID"
|
||||
// @Param trash query bool false "Move to trash instead of permanent delete" default(false)
|
||||
// @Success 204 {string} string "Directory deleted"
|
||||
// @Failure 401 {string} string "Not authenticated"
|
||||
// @Failure 404 {string} string "Directory not found"
|
||||
// @Router /accounts/{accountID}/directories/{directoryID} [delete]
|
||||
func (h *HTTPHandler) deleteDirectory(c *fiber.Ctx) error {
|
||||
node := mustCurrentDirectoryNode(c)
|
||||
|
||||
@@ -259,6 +342,21 @@ func (h *HTTPHandler) deleteDirectory(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
// moveItemsToDirectory moves files and directories into this directory
|
||||
// @Summary Move items to directory
|
||||
// @Description Move one or more files or directories into this directory. All items must currently be in the same source directory.
|
||||
// @Tags directories
|
||||
// @Accept json
|
||||
// @Security BearerAuth
|
||||
// @Param accountID path string true "Account ID" format(uuid)
|
||||
// @Param directoryID path string true "Target directory ID"
|
||||
// @Param request body postDirectoryContentRequest true "Items to move"
|
||||
// @Success 204 {string} string "Items moved successfully"
|
||||
// @Failure 400 {object} map[string]string "Invalid request or items not in same directory"
|
||||
// @Failure 401 {string} string "Not authenticated"
|
||||
// @Failure 404 {object} map[string]string "One or more items not found"
|
||||
// @Failure 409 {object} map[string]string "Name conflict in target directory"
|
||||
// @Router /accounts/{accountID}/directories/{directoryID}/content [post]
|
||||
func (h *HTTPHandler) moveItemsToDirectory(c *fiber.Ctx) error {
|
||||
acc := account.CurrentAccount(c)
|
||||
if acc == nil {
|
||||
|
||||
@@ -11,15 +11,25 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// FileInfo represents file metadata
|
||||
// @Description File information including name, size, and timestamps
|
||||
type FileInfo struct {
|
||||
Kind string `json:"kind"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
MimeType string `json:"mimeType"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
DeletedAt *time.Time `json:"deletedAt,omitempty"`
|
||||
// Item type, always "file"
|
||||
Kind string `json:"kind" example:"file"`
|
||||
// Unique file identifier
|
||||
ID string `json:"id" example:"mElnUNCm8F22"`
|
||||
// File name
|
||||
Name string `json:"name" example:"document.pdf"`
|
||||
// File size in bytes
|
||||
Size int64 `json:"size" example:"1048576"`
|
||||
// MIME type of the file
|
||||
MimeType string `json:"mimeType" example:"application/pdf"`
|
||||
// When the file was created (ISO 8601)
|
||||
CreatedAt time.Time `json:"createdAt" example:"2024-12-13T15:04:05Z"`
|
||||
// When the file was last updated (ISO 8601)
|
||||
UpdatedAt time.Time `json:"updatedAt" example:"2024-12-13T16:30:00Z"`
|
||||
// When the file was trashed, null if not trashed (ISO 8601)
|
||||
DeletedAt *time.Time `json:"deletedAt,omitempty" example:"2024-12-14T10:00:00Z"`
|
||||
}
|
||||
|
||||
func mustCurrentFileNode(c *fiber.Ctx) *virtualfs.Node {
|
||||
@@ -46,6 +56,18 @@ func (h *HTTPHandler) currentFileMiddleware(c *fiber.Ctx) error {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// fetchFile returns file metadata
|
||||
// @Summary Get file info
|
||||
// @Description Retrieve metadata for a specific file
|
||||
// @Tags files
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param accountID path string true "Account ID" format(uuid)
|
||||
// @Param fileID path string true "File ID"
|
||||
// @Success 200 {object} FileInfo "File metadata"
|
||||
// @Failure 401 {string} string "Not authenticated"
|
||||
// @Failure 404 {string} string "File not found"
|
||||
// @Router /accounts/{accountID}/files/{fileID} [get]
|
||||
func (h *HTTPHandler) fetchFile(c *fiber.Ctx) error {
|
||||
node := mustCurrentFileNode(c)
|
||||
i := FileInfo{
|
||||
@@ -61,6 +83,19 @@ func (h *HTTPHandler) fetchFile(c *fiber.Ctx) error {
|
||||
return c.JSON(i)
|
||||
}
|
||||
|
||||
// downloadFile streams file content
|
||||
// @Summary Download file
|
||||
// @Description Download the file content. May redirect to a signed URL for external storage.
|
||||
// @Tags files
|
||||
// @Produce application/octet-stream
|
||||
// @Security BearerAuth
|
||||
// @Param accountID path string true "Account ID" format(uuid)
|
||||
// @Param fileID path string true "File ID"
|
||||
// @Success 200 {file} binary "File content stream"
|
||||
// @Success 307 {string} string "Redirect to download URL"
|
||||
// @Failure 401 {string} string "Not authenticated"
|
||||
// @Failure 404 {string} string "File not found"
|
||||
// @Router /accounts/{accountID}/files/{fileID}/content [get]
|
||||
func (h *HTTPHandler) downloadFile(c *fiber.Ctx) error {
|
||||
node := mustCurrentFileNode(c)
|
||||
|
||||
@@ -89,6 +124,21 @@ func (h *HTTPHandler) downloadFile(c *fiber.Ctx) error {
|
||||
return httperr.Internal(errors.New("vfs returned neither a reader nor a URL"))
|
||||
}
|
||||
|
||||
// patchFile updates file properties
|
||||
// @Summary Update file
|
||||
// @Description Update file properties such as name (rename)
|
||||
// @Tags files
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param accountID path string true "Account ID" format(uuid)
|
||||
// @Param fileID path string true "File ID"
|
||||
// @Param request body patchFileRequest true "File update"
|
||||
// @Success 200 {object} FileInfo "Updated file metadata"
|
||||
// @Failure 400 {object} map[string]string "Invalid request"
|
||||
// @Failure 401 {string} string "Not authenticated"
|
||||
// @Failure 404 {string} string "File not found"
|
||||
// @Router /accounts/{accountID}/files/{fileID} [patch]
|
||||
func (h *HTTPHandler) patchFile(c *fiber.Ctx) error {
|
||||
node := mustCurrentFileNode(c)
|
||||
|
||||
@@ -131,6 +181,20 @@ func (h *HTTPHandler) patchFile(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
// deleteFile removes a file
|
||||
// @Summary Delete file
|
||||
// @Description Delete a file permanently or move it to trash
|
||||
// @Tags files
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param accountID path string true "Account ID" format(uuid)
|
||||
// @Param fileID path string true "File ID"
|
||||
// @Param trash query bool false "Move to trash instead of permanent delete" default(false)
|
||||
// @Success 200 {object} FileInfo "Trashed file info (when trash=true)"
|
||||
// @Success 204 {string} string "Permanently deleted (when trash=false)"
|
||||
// @Failure 401 {string} string "Not authenticated"
|
||||
// @Failure 404 {string} string "File not found"
|
||||
// @Router /accounts/{accountID}/files/{fileID} [delete]
|
||||
func (h *HTTPHandler) deleteFile(c *fiber.Ctx) error {
|
||||
node := mustCurrentFileNode(c)
|
||||
|
||||
|
||||
@@ -11,12 +11,18 @@ type HTTPHandler struct {
|
||||
db *bun.DB
|
||||
}
|
||||
|
||||
// patchFileRequest represents a file update request
|
||||
// @Description Request to update file properties
|
||||
type patchFileRequest struct {
|
||||
Name string `json:"name"`
|
||||
// New name for the file
|
||||
Name string `json:"name" example:"renamed-document.pdf"`
|
||||
}
|
||||
|
||||
// patchDirectoryRequest represents a directory update request
|
||||
// @Description Request to update directory properties
|
||||
type patchDirectoryRequest struct {
|
||||
Name string `json:"name"`
|
||||
// New name for the directory
|
||||
Name string `json:"name" example:"My Documents"`
|
||||
}
|
||||
|
||||
func NewHTTPHandler(vfs *virtualfs.VirtualFS, db *bun.DB) *HTTPHandler {
|
||||
|
||||
@@ -10,13 +10,20 @@ import (
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
// createUploadRequest represents a new upload session request
|
||||
// @Description Request to initiate a file upload
|
||||
type createUploadRequest struct {
|
||||
ParentID string `json:"parentId"`
|
||||
Name string `json:"name"`
|
||||
// ID of the parent directory to upload into
|
||||
ParentID string `json:"parentId" example:"kRp2XYTq9A55"`
|
||||
// Name of the file being uploaded
|
||||
Name string `json:"name" example:"document.pdf"`
|
||||
}
|
||||
|
||||
// updateUploadRequest represents an upload status update
|
||||
// @Description Request to update upload status (e.g., mark as completed)
|
||||
type updateUploadRequest struct {
|
||||
Status Status `json:"status"`
|
||||
// New status for the upload
|
||||
Status Status `json:"status" example:"completed" enums:"completed"`
|
||||
}
|
||||
|
||||
type HTTPHandler struct {
|
||||
@@ -36,6 +43,21 @@ func (h *HTTPHandler) RegisterRoutes(api fiber.Router) {
|
||||
upload.Patch("/:uploadID", h.Update)
|
||||
}
|
||||
|
||||
// Create initiates a new file upload session
|
||||
// @Summary Create upload session
|
||||
// @Description Start a new file upload session. Returns an upload URL to PUT file content to.
|
||||
// @Tags uploads
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param accountID path string true "Account ID" format(uuid)
|
||||
// @Param request body createUploadRequest true "Upload details"
|
||||
// @Success 200 {object} Upload "Upload session created"
|
||||
// @Failure 400 {object} map[string]string "Parent is not a directory"
|
||||
// @Failure 401 {string} string "Not authenticated"
|
||||
// @Failure 404 {string} string "Parent directory not found"
|
||||
// @Failure 409 {object} map[string]string "File with this name already exists"
|
||||
// @Router /accounts/{accountID}/uploads [post]
|
||||
func (h *HTTPHandler) Create(c *fiber.Ctx) error {
|
||||
account := account.CurrentAccount(c)
|
||||
if account == nil {
|
||||
@@ -71,6 +93,19 @@ func (h *HTTPHandler) Create(c *fiber.Ctx) error {
|
||||
return c.JSON(upload)
|
||||
}
|
||||
|
||||
// ReceiveContent receives the file content for an upload
|
||||
// @Summary Upload file content
|
||||
// @Description Stream file content to complete an upload. Send raw binary data in the request body.
|
||||
// @Tags uploads
|
||||
// @Accept application/octet-stream
|
||||
// @Security BearerAuth
|
||||
// @Param accountID path string true "Account ID" format(uuid)
|
||||
// @Param uploadID path string true "Upload session ID"
|
||||
// @Param file body []byte true "File content (binary)"
|
||||
// @Success 204 {string} string "Content received successfully"
|
||||
// @Failure 401 {string} string "Not authenticated"
|
||||
// @Failure 404 {string} string "Upload session not found"
|
||||
// @Router /accounts/{accountID}/uploads/{uploadID}/content [put]
|
||||
func (h *HTTPHandler) ReceiveContent(c *fiber.Ctx) error {
|
||||
account := account.CurrentAccount(c)
|
||||
if account == nil {
|
||||
@@ -91,6 +126,21 @@ func (h *HTTPHandler) ReceiveContent(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
// Update updates the upload status
|
||||
// @Summary Complete upload
|
||||
// @Description Mark an upload as completed after content has been uploaded. This finalizes the file in the filesystem.
|
||||
// @Tags uploads
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param accountID path string true "Account ID" format(uuid)
|
||||
// @Param uploadID path string true "Upload session ID"
|
||||
// @Param request body updateUploadRequest true "Status update"
|
||||
// @Success 200 {object} Upload "Upload completed"
|
||||
// @Failure 400 {object} map[string]string "Content not uploaded yet or invalid status"
|
||||
// @Failure 401 {string} string "Not authenticated"
|
||||
// @Failure 404 {string} string "Upload session not found"
|
||||
// @Router /accounts/{accountID}/uploads/{uploadID} [patch]
|
||||
func (h *HTTPHandler) Update(c *fiber.Ctx) error {
|
||||
account := account.CurrentAccount(c)
|
||||
if account == nil {
|
||||
|
||||
@@ -2,17 +2,28 @@ package upload
|
||||
|
||||
import "github.com/get-drexa/drexa/internal/virtualfs"
|
||||
|
||||
// Status represents the upload state
|
||||
// @Description Upload status enumeration
|
||||
type Status string
|
||||
|
||||
const (
|
||||
// StatusPending indicates upload is awaiting content
|
||||
StatusPending Status = "pending"
|
||||
// StatusCompleted indicates upload finished successfully
|
||||
StatusCompleted Status = "completed"
|
||||
// StatusFailed indicates upload failed
|
||||
StatusFailed Status = "failed"
|
||||
)
|
||||
|
||||
// Upload represents a file upload session
|
||||
// @Description File upload session with status and upload URL
|
||||
type Upload struct {
|
||||
ID string `json:"id"`
|
||||
Status Status `json:"status"`
|
||||
TargetNode *virtualfs.Node `json:"-"`
|
||||
UploadURL string `json:"uploadUrl"`
|
||||
// Unique upload session identifier
|
||||
ID string `json:"id" example:"xNq5RVBt3K88"`
|
||||
// Current upload status
|
||||
Status Status `json:"status" example:"pending" enums:"pending,completed,failed"`
|
||||
// Internal target node reference
|
||||
TargetNode *virtualfs.Node `json:"-" swaggerignore:"true"`
|
||||
// URL to upload file content to
|
||||
UploadURL string `json:"uploadUrl" example:"https://api.example.com/api/accounts/550e8400-e29b-41d4-a716-446655440000/uploads/xNq5RVBt3K88/content"`
|
||||
}
|
||||
|
||||
@@ -22,6 +22,15 @@ func (h *HTTPHandler) RegisterRoutes(api fiber.Router) {
|
||||
user.Get("/me", h.getAuthenticatedUser)
|
||||
}
|
||||
|
||||
// getAuthenticatedUser returns the currently authenticated user
|
||||
// @Summary Get current user
|
||||
// @Description Retrieve the authenticated user's profile information
|
||||
// @Tags users
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} User "User profile"
|
||||
// @Failure 401 {string} string "Not authenticated"
|
||||
// @Router /users/me [get]
|
||||
func (h *HTTPHandler) getAuthenticatedUser(c *fiber.Ctx) error {
|
||||
u := reqctx.AuthenticatedUser(c).(*User)
|
||||
if u == nil {
|
||||
|
||||
@@ -8,15 +8,20 @@ import (
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
// User represents a user account in the system
|
||||
// @Description User account information
|
||||
type User struct {
|
||||
bun.BaseModel `bun:"users"`
|
||||
bun.BaseModel `bun:"users" swaggerignore:"true"`
|
||||
|
||||
ID uuid.UUID `bun:",pk,type:uuid" json:"id"`
|
||||
DisplayName string `bun:"display_name" json:"displayName"`
|
||||
Email string `bun:"email,unique,notnull" json:"email"`
|
||||
Password password.Hashed `bun:"password,notnull" json:"-"`
|
||||
CreatedAt time.Time `bun:"created_at,notnull,nullzero" json:"-"`
|
||||
UpdatedAt time.Time `bun:"updated_at,notnull,nullzero" json:"-"`
|
||||
// Unique user identifier
|
||||
ID uuid.UUID `bun:",pk,type:uuid" json:"id" example:"550e8400-e29b-41d4-a716-446655440000"`
|
||||
// User's display name
|
||||
DisplayName string `bun:"display_name" json:"displayName" example:"John Doe"`
|
||||
// User's email address
|
||||
Email string `bun:"email,unique,notnull" json:"email" example:"john@example.com"`
|
||||
Password password.Hashed `bun:"password,notnull" json:"-" swaggerignore:"true"`
|
||||
CreatedAt time.Time `bun:"created_at,notnull,nullzero" json:"-" swaggerignore:"true"`
|
||||
UpdatedAt time.Time `bun:"updated_at,notnull,nullzero" json:"-" swaggerignore:"true"`
|
||||
}
|
||||
|
||||
func newUserID() (uuid.UUID, error) {
|
||||
|
||||
Reference in New Issue
Block a user