From f698e5a8a9169de9bf036c7b402d90f4d5628901 Mon Sep 17 00:00:00 2001 From: saltbo Date: Wed, 3 Jun 2026 11:12:56 -0400 Subject: [PATCH] feat(downloads): show normalized task details --- docs/openapi/downloader.json | 776 ++++ downloader/go.mod | 2 +- downloader/go.sum | 2 + downloader/internal/client/client.go | 83 +- downloader/internal/engine/engine.go | 161 +- downloader/internal/engine/engine_test.go | 11 +- .../internal/engine/live_download_test.go | 2 +- downloader/internal/openapi/client.gen.go | 654 ++- downloader/internal/worker/worker.go | 3 +- migrations/0036_add-download-task-detail.sql | 1 + migrations/meta/0036_snapshot.json | 3914 +++++++++++++++++ migrations/meta/_journal.json | 7 + server/db/schema.ts | 1 + .../routes/download-tasks.integration.test.ts | 17 + server/routes/download-tasks.ts | 9 +- server/services/downloads.ts | 14 +- server/test/setup.ts | 1 + shared/schemas/downloads.ts | 43 + shared/schemas/index.ts | 2 + shared/types/index.ts | 42 + src/i18n/locales/en.json | 20 + src/i18n/locales/zh.json | 20 + src/routes/_authenticated/downloads/index.tsx | 291 +- 23 files changed, 5894 insertions(+), 182 deletions(-) create mode 100644 migrations/0036_add-download-task-detail.sql create mode 100644 migrations/meta/0036_snapshot.json diff --git a/docs/openapi/downloader.json b/docs/openapi/downloader.json index 803c27c2..575cbf49 100644 --- a/docs/openapi/downloader.json +++ b/docs/openapi/downloader.json @@ -321,10 +321,166 @@ "type": "integer", "format": "int64" }, + "errorMessage": { + "type": "string", + "nullable": true + }, "resultObjectId": { "type": "string", "nullable": true }, + "detail": { + "type": "object", + "nullable": true, + "properties": { + "engine": { + "type": "string", + "enum": [ + "builtin", + "aria2", + "qbittorrent" + ] + }, + "phase": { + "type": "string", + "maxLength": 80 + }, + "message": { + "type": "string", + "maxLength": 500 + }, + "etaSeconds": { + "type": "integer", + "nullable": true, + "minimum": 0 + }, + "connections": { + "type": "integer", + "minimum": 0 + }, + "infoHash": { + "type": "string", + "maxLength": 120 + }, + "torrentName": { + "type": "string", + "maxLength": 255 + }, + "seeders": { + "type": "integer", + "minimum": 0 + }, + "leechers": { + "type": "integer", + "minimum": 0 + }, + "peers": { + "type": "integer", + "minimum": 0 + }, + "uploadedBytes": { + "type": "integer", + "minimum": 0 + }, + "trackers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "maxLength": 1024 + }, + "status": { + "type": "string", + "maxLength": 80 + }, + "peers": { + "type": "integer", + "minimum": 0 + }, + "seeds": { + "type": "integer", + "minimum": 0 + }, + "leechers": { + "type": "integer", + "minimum": 0 + }, + "message": { + "type": "string", + "maxLength": 500 + } + }, + "required": [ + "url" + ] + }, + "maxItems": 20 + }, + "peerSamples": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { + "type": "string", + "maxLength": 160 + }, + "client": { + "type": "string", + "maxLength": 160 + }, + "progress": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "downloadBps": { + "type": "integer", + "minimum": 0 + }, + "uploadBps": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "address" + ] + }, + "maxItems": 20 + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "maxLength": 1024 + }, + "size": { + "type": "integer", + "minimum": 0 + }, + "completedBytes": { + "type": "integer", + "minimum": 0 + }, + "selected": { + "type": "boolean" + } + }, + "required": [ + "path", + "size" + ] + }, + "maxItems": 50 + } + } + }, "uploadToken": { "type": "string" }, @@ -492,10 +648,166 @@ "type": "integer", "format": "int64" }, + "errorMessage": { + "type": "string", + "nullable": true + }, "resultObjectId": { "type": "string", "nullable": true }, + "detail": { + "type": "object", + "nullable": true, + "properties": { + "engine": { + "type": "string", + "enum": [ + "builtin", + "aria2", + "qbittorrent" + ] + }, + "phase": { + "type": "string", + "maxLength": 80 + }, + "message": { + "type": "string", + "maxLength": 500 + }, + "etaSeconds": { + "type": "integer", + "nullable": true, + "minimum": 0 + }, + "connections": { + "type": "integer", + "minimum": 0 + }, + "infoHash": { + "type": "string", + "maxLength": 120 + }, + "torrentName": { + "type": "string", + "maxLength": 255 + }, + "seeders": { + "type": "integer", + "minimum": 0 + }, + "leechers": { + "type": "integer", + "minimum": 0 + }, + "peers": { + "type": "integer", + "minimum": 0 + }, + "uploadedBytes": { + "type": "integer", + "minimum": 0 + }, + "trackers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "maxLength": 1024 + }, + "status": { + "type": "string", + "maxLength": 80 + }, + "peers": { + "type": "integer", + "minimum": 0 + }, + "seeds": { + "type": "integer", + "minimum": 0 + }, + "leechers": { + "type": "integer", + "minimum": 0 + }, + "message": { + "type": "string", + "maxLength": 500 + } + }, + "required": [ + "url" + ] + }, + "maxItems": 20 + }, + "peerSamples": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { + "type": "string", + "maxLength": 160 + }, + "client": { + "type": "string", + "maxLength": 160 + }, + "progress": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "downloadBps": { + "type": "integer", + "minimum": 0 + }, + "uploadBps": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "address" + ] + }, + "maxItems": 20 + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "maxLength": 1024 + }, + "size": { + "type": "integer", + "minimum": 0 + }, + "completedBytes": { + "type": "integer", + "minimum": 0 + }, + "selected": { + "type": "boolean" + } + }, + "required": [ + "path", + "size" + ] + }, + "maxItems": 50 + } + } + }, "uploadToken": { "type": "string" }, @@ -647,10 +959,166 @@ "type": "integer", "format": "int64" }, + "errorMessage": { + "type": "string", + "nullable": true + }, "resultObjectId": { "type": "string", "nullable": true }, + "detail": { + "type": "object", + "nullable": true, + "properties": { + "engine": { + "type": "string", + "enum": [ + "builtin", + "aria2", + "qbittorrent" + ] + }, + "phase": { + "type": "string", + "maxLength": 80 + }, + "message": { + "type": "string", + "maxLength": 500 + }, + "etaSeconds": { + "type": "integer", + "nullable": true, + "minimum": 0 + }, + "connections": { + "type": "integer", + "minimum": 0 + }, + "infoHash": { + "type": "string", + "maxLength": 120 + }, + "torrentName": { + "type": "string", + "maxLength": 255 + }, + "seeders": { + "type": "integer", + "minimum": 0 + }, + "leechers": { + "type": "integer", + "minimum": 0 + }, + "peers": { + "type": "integer", + "minimum": 0 + }, + "uploadedBytes": { + "type": "integer", + "minimum": 0 + }, + "trackers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "maxLength": 1024 + }, + "status": { + "type": "string", + "maxLength": 80 + }, + "peers": { + "type": "integer", + "minimum": 0 + }, + "seeds": { + "type": "integer", + "minimum": 0 + }, + "leechers": { + "type": "integer", + "minimum": 0 + }, + "message": { + "type": "string", + "maxLength": 500 + } + }, + "required": [ + "url" + ] + }, + "maxItems": 20 + }, + "peerSamples": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { + "type": "string", + "maxLength": 160 + }, + "client": { + "type": "string", + "maxLength": 160 + }, + "progress": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "downloadBps": { + "type": "integer", + "minimum": 0 + }, + "uploadBps": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "address" + ] + }, + "maxItems": 20 + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "maxLength": 1024 + }, + "size": { + "type": "integer", + "minimum": 0 + }, + "completedBytes": { + "type": "integer", + "minimum": 0 + }, + "selected": { + "type": "boolean" + } + }, + "required": [ + "path", + "size" + ] + }, + "maxItems": 50 + } + } + }, "uploadToken": { "type": "string" }, @@ -752,6 +1220,158 @@ "type": "string", "nullable": true, "minLength": 1 + }, + "detail": { + "type": "object", + "nullable": true, + "properties": { + "engine": { + "type": "string", + "enum": [ + "builtin", + "aria2", + "qbittorrent" + ] + }, + "phase": { + "type": "string", + "maxLength": 80 + }, + "message": { + "type": "string", + "maxLength": 500 + }, + "etaSeconds": { + "type": "integer", + "nullable": true, + "minimum": 0 + }, + "connections": { + "type": "integer", + "minimum": 0 + }, + "infoHash": { + "type": "string", + "maxLength": 120 + }, + "torrentName": { + "type": "string", + "maxLength": 255 + }, + "seeders": { + "type": "integer", + "minimum": 0 + }, + "leechers": { + "type": "integer", + "minimum": 0 + }, + "peers": { + "type": "integer", + "minimum": 0 + }, + "uploadedBytes": { + "type": "integer", + "minimum": 0 + }, + "trackers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "maxLength": 1024 + }, + "status": { + "type": "string", + "maxLength": 80 + }, + "peers": { + "type": "integer", + "minimum": 0 + }, + "seeds": { + "type": "integer", + "minimum": 0 + }, + "leechers": { + "type": "integer", + "minimum": 0 + }, + "message": { + "type": "string", + "maxLength": 500 + } + }, + "required": [ + "url" + ] + }, + "maxItems": 20 + }, + "peerSamples": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { + "type": "string", + "maxLength": 160 + }, + "client": { + "type": "string", + "maxLength": 160 + }, + "progress": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "downloadBps": { + "type": "integer", + "minimum": 0 + }, + "uploadBps": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "address" + ] + }, + "maxItems": 20 + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "maxLength": 1024 + }, + "size": { + "type": "integer", + "minimum": 0 + }, + "completedBytes": { + "type": "integer", + "minimum": 0 + }, + "selected": { + "type": "boolean" + } + }, + "required": [ + "path", + "size" + ] + }, + "maxItems": 50 + } + } } } } @@ -816,10 +1436,166 @@ "type": "integer", "format": "int64" }, + "errorMessage": { + "type": "string", + "nullable": true + }, "resultObjectId": { "type": "string", "nullable": true }, + "detail": { + "type": "object", + "nullable": true, + "properties": { + "engine": { + "type": "string", + "enum": [ + "builtin", + "aria2", + "qbittorrent" + ] + }, + "phase": { + "type": "string", + "maxLength": 80 + }, + "message": { + "type": "string", + "maxLength": 500 + }, + "etaSeconds": { + "type": "integer", + "nullable": true, + "minimum": 0 + }, + "connections": { + "type": "integer", + "minimum": 0 + }, + "infoHash": { + "type": "string", + "maxLength": 120 + }, + "torrentName": { + "type": "string", + "maxLength": 255 + }, + "seeders": { + "type": "integer", + "minimum": 0 + }, + "leechers": { + "type": "integer", + "minimum": 0 + }, + "peers": { + "type": "integer", + "minimum": 0 + }, + "uploadedBytes": { + "type": "integer", + "minimum": 0 + }, + "trackers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "maxLength": 1024 + }, + "status": { + "type": "string", + "maxLength": 80 + }, + "peers": { + "type": "integer", + "minimum": 0 + }, + "seeds": { + "type": "integer", + "minimum": 0 + }, + "leechers": { + "type": "integer", + "minimum": 0 + }, + "message": { + "type": "string", + "maxLength": 500 + } + }, + "required": [ + "url" + ] + }, + "maxItems": 20 + }, + "peerSamples": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { + "type": "string", + "maxLength": 160 + }, + "client": { + "type": "string", + "maxLength": 160 + }, + "progress": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "downloadBps": { + "type": "integer", + "minimum": 0 + }, + "uploadBps": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "address" + ] + }, + "maxItems": 20 + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "maxLength": 1024 + }, + "size": { + "type": "integer", + "minimum": 0 + }, + "completedBytes": { + "type": "integer", + "minimum": 0 + }, + "selected": { + "type": "boolean" + } + }, + "required": [ + "path", + "size" + ] + }, + "maxItems": 50 + } + } + }, "uploadToken": { "type": "string" }, diff --git a/downloader/go.mod b/downloader/go.mod index 33d49f1b..5a512b08 100644 --- a/downloader/go.mod +++ b/downloader/go.mod @@ -5,6 +5,7 @@ go 1.25.0 require ( github.com/Braurbeki/arigo v0.0.7 github.com/autobrr/go-qbittorrent v1.16.0 + github.com/oapi-codegen/runtime v1.4.1 github.com/spf13/cobra v1.10.1 github.com/spf13/viper v1.21.0 ) @@ -20,7 +21,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.4.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/oapi-codegen/runtime v1.4.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect diff --git a/downloader/go.sum b/downloader/go.sum index 341ea21d..dc024b1c 100644 --- a/downloader/go.sum +++ b/downloader/go.sum @@ -39,6 +39,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs= +github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= github.com/oapi-codegen/runtime v1.4.1 h1:9nwLoI+KrWxzbBcp0jO/R8uXqbik/HUyCvPeU68Y/qo= github.com/oapi-codegen/runtime v1.4.1/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= diff --git a/downloader/internal/client/client.go b/downloader/internal/client/client.go index f300e0b8..8d447401 100644 --- a/downloader/internal/client/client.go +++ b/downloader/internal/client/client.go @@ -26,19 +26,61 @@ type Page[T any] struct { } type DownloadTask struct { - ID string `json:"id"` - SourceType string `json:"sourceType"` - SourceURI string `json:"sourceUri"` - Name string `json:"name"` - TargetFolder string `json:"targetFolder"` - Status string `json:"status"` - DownloadedBytes int64 `json:"downloadedBytes"` - TotalBytes *int64 `json:"totalBytes"` - DownloadBps int64 `json:"downloadBps"` - UploadBps int64 `json:"uploadBps"` - ResultObjectID string `json:"resultObjectId"` - UploadToken string `json:"uploadToken"` - AssignedDownloaderID string `json:"assignedDownloaderId"` + ID string `json:"id"` + SourceType string `json:"sourceType"` + SourceURI string `json:"sourceUri"` + Name string `json:"name"` + TargetFolder string `json:"targetFolder"` + Status string `json:"status"` + DownloadedBytes int64 `json:"downloadedBytes"` + TotalBytes *int64 `json:"totalBytes"` + DownloadBps int64 `json:"downloadBps"` + UploadBps int64 `json:"uploadBps"` + Detail *DownloadTaskDetail `json:"detail"` + ResultObjectID string `json:"resultObjectId"` + UploadToken string `json:"uploadToken"` + AssignedDownloaderID string `json:"assignedDownloaderId"` +} + +type DownloadTaskDetail struct { + Engine string `json:"engine,omitempty"` + Phase string `json:"phase,omitempty"` + Message string `json:"message,omitempty"` + ETASeconds *int64 `json:"etaSeconds,omitempty"` + Connections *int64 `json:"connections,omitempty"` + InfoHash string `json:"infoHash,omitempty"` + TorrentName string `json:"torrentName,omitempty"` + Seeders *int64 `json:"seeders,omitempty"` + Leechers *int64 `json:"leechers,omitempty"` + Peers *int64 `json:"peers,omitempty"` + UploadedBytes *int64 `json:"uploadedBytes,omitempty"` + Trackers []DownloadTaskTracker `json:"trackers,omitempty"` + PeerSamples []DownloadTaskPeer `json:"peerSamples,omitempty"` + Files []DownloadTaskFile `json:"files,omitempty"` +} + +type DownloadTaskTracker struct { + URL string `json:"url"` + Status string `json:"status,omitempty"` + Peers *int64 `json:"peers,omitempty"` + Seeds *int64 `json:"seeds,omitempty"` + Leechers *int64 `json:"leechers,omitempty"` + Message string `json:"message,omitempty"` +} + +type DownloadTaskPeer struct { + Address string `json:"address"` + Client string `json:"client,omitempty"` + Progress *float64 `json:"progress,omitempty"` + DownloadBps *int64 `json:"downloadBps,omitempty"` + UploadBps *int64 `json:"uploadBps,omitempty"` +} + +type DownloadTaskFile struct { + Path string `json:"path"` + Size int64 `json:"size"` + CompletedBytes *int64 `json:"completedBytes,omitempty"` + Selected *bool `json:"selected,omitempty"` } type Heartbeat struct { @@ -56,13 +98,14 @@ type Heartbeat struct { } type TaskPatch struct { - Status string `json:"status,omitempty"` - DownloadedBytes *int64 `json:"downloadedBytes,omitempty"` - TotalBytes *int64 `json:"totalBytes,omitempty"` - DownloadBps *int64 `json:"downloadBps,omitempty"` - UploadBps *int64 `json:"uploadBps,omitempty"` - ErrorMessage *string `json:"errorMessage,omitempty"` - ResultObjectID *string `json:"resultObjectId,omitempty"` + Status string `json:"status,omitempty"` + DownloadedBytes *int64 `json:"downloadedBytes,omitempty"` + TotalBytes *int64 `json:"totalBytes,omitempty"` + DownloadBps *int64 `json:"downloadBps,omitempty"` + UploadBps *int64 `json:"uploadBps,omitempty"` + ErrorMessage *string `json:"errorMessage,omitempty"` + ResultObjectID *string `json:"resultObjectId,omitempty"` + Detail *DownloadTaskDetail `json:"detail,omitempty"` } type ObjectDraft struct { diff --git a/downloader/internal/engine/engine.go b/downloader/internal/engine/engine.go index edf633c2..eb6a0623 100644 --- a/downloader/internal/engine/engine.go +++ b/downloader/internal/engine/engine.go @@ -24,7 +24,7 @@ type Result struct { Size int64 } -type Progress func(downloaded int64, total *int64, bps int64) error +type Progress func(downloaded int64, total *int64, bps int64, detail *client.DownloadTaskDetail) error type Engine interface { Check(ctx context.Context) error @@ -88,7 +88,7 @@ func (h HTTP) Download(ctx context.Context, task client.DownloadTask, progress P if _, err := io.Copy(file, io.TeeReader(res.Body, counter)); err != nil { return Result{}, err } - if err := progress(counter.downloaded, total, 0); err != nil { + if err := progress(counter.downloaded, total, 0, &client.DownloadTaskDetail{Engine: "builtin", Phase: "completed"}); err != nil { return Result{}, err } return Result{Path: path, Name: name, Size: counter.downloaded}, nil @@ -146,7 +146,7 @@ func (a Aria2) Download(ctx context.Context, task client.DownloadTask, progress } initialProgress := progress if task.SourceType != "http" { - initialProgress = func(downloaded int64, total *int64, bps int64) error { return nil } + initialProgress = func(downloaded int64, total *int64, bps int64, detail *client.DownloadTaskDetail) error { return nil } } status, err := a.waitAria2(ctx, &aria, gid.GID, initialProgress) if err != nil { @@ -273,7 +273,7 @@ func (p *progressWriter) Write(data []byte) (int, error) { now := time.Now() if now.Sub(p.lastAt) >= time.Second { bps := int64(float64(p.downloaded-p.lastBytes) / now.Sub(p.lastAt).Seconds()) - if err := p.progress(p.downloaded, p.total, bps); err != nil { + if err := p.progress(p.downloaded, p.total, bps, &client.DownloadTaskDetail{Engine: "builtin", Phase: "downloading"}); err != nil { return n, err } p.lastBytes = p.downloaded @@ -307,7 +307,7 @@ func (a Aria2) waitAria2(ctx context.Context, aria **arigo.Client, gid string, p if total > 0 { totalPtr = &total } - if err := progress(completed, totalPtr, bps); err != nil { + if err := progress(completed, totalPtr, bps, aria2Detail(status)); err != nil { _ = (*aria).ForcePause(gid) return arigo.Status{}, err } @@ -359,6 +359,70 @@ func isAria2RPCDisconnected(err error) bool { return errors.Is(err, rpc2.ErrShutdown) || errors.Is(err, io.ErrClosedPipe) || strings.Contains(err.Error(), "connection is shut down") } +func aria2Detail(status arigo.Status) *client.DownloadTaskDetail { + connections := int64(status.Connections) + seeders := int64(status.NumSeeders) + uploaded := int64(status.UploadLength) + detail := &client.DownloadTaskDetail{ + Engine: "aria2", + Phase: string(status.Status), + Connections: &connections, + InfoHash: status.InfoHash, + TorrentName: status.BitTorrent.Info.Name, + Seeders: &seeders, + UploadedBytes: &uploaded, + Trackers: aria2Trackers(status.BitTorrent.AnnounceList), + Files: aria2Files(status.Files), + } + if status.ErrorMessage != "" { + detail.Message = status.ErrorMessage + } + return detail +} + +func aria2Trackers(announceList [][]string) []client.DownloadTaskTracker { + trackers := make([]client.DownloadTaskTracker, 0, 20) + seen := map[string]struct{}{} + for _, tier := range announceList { + for _, url := range tier { + if url == "" { + continue + } + if _, exists := seen[url]; exists { + continue + } + seen[url] = struct{}{} + trackers = append(trackers, client.DownloadTaskTracker{URL: url}) + if len(trackers) >= 20 { + return trackers + } + } + } + return trackers +} + +func aria2Files(files []arigo.File) []client.DownloadTaskFile { + out := make([]client.DownloadTaskFile, 0, min(len(files), 50)) + for _, file := range files { + if file.Path == "" || isAria2MetadataPath(file.Path) { + continue + } + size := int64(file.Length) + completed := int64(file.CompletedLength) + selected := file.Selected + out = append(out, client.DownloadTaskFile{ + Path: file.Path, + Size: size, + CompletedBytes: &completed, + Selected: &selected, + }) + if len(out) >= 50 { + break + } + } + return out +} + func hasAria2LocalFile(files []arigo.File) bool { for _, file := range files { if file.Path != "" && !isAria2MetadataPath(file.Path) { @@ -397,7 +461,7 @@ func waitQBittorrent( if total > 0 { totalPtr = &total } - if err := progress(torrent.Completed, totalPtr, torrent.DlSpeed); err != nil { + if err := progress(torrent.Completed, totalPtr, torrent.DlSpeed, qbittorrentDetail(ctx, qbt, torrent)); err != nil { _ = qbt.StopCtx(ctx, []string{torrent.Hash}) return qbittorrent.Torrent{}, err } @@ -411,6 +475,91 @@ func waitQBittorrent( } } +func qbittorrentDetail(ctx context.Context, qbt *qbittorrent.Client, torrent qbittorrent.Torrent) *client.DownloadTaskDetail { + connections := int64(torrent.NumSeeds + torrent.NumLeechs) + seeders := torrent.NumSeeds + leechers := torrent.NumLeechs + peers := torrent.NumComplete + torrent.NumIncomplete + uploaded := torrent.Uploaded + var eta *int64 + if torrent.ETA >= 0 { + eta = &torrent.ETA + } + return &client.DownloadTaskDetail{ + Engine: "qbittorrent", + Phase: string(torrent.State), + ETASeconds: eta, + Connections: &connections, + InfoHash: torrent.Hash, + TorrentName: torrent.Name, + Seeders: &seeders, + Leechers: &leechers, + Peers: &peers, + UploadedBytes: &uploaded, + Trackers: qbittorrentTrackers(ctx, qbt, torrent), + PeerSamples: qbittorrentPeers(ctx, qbt, torrent.Hash), + } +} + +func qbittorrentTrackers(ctx context.Context, qbt *qbittorrent.Client, torrent qbittorrent.Torrent) []client.DownloadTaskTracker { + trackers := torrent.Trackers + if len(trackers) == 0 && torrent.Hash != "" { + loaded, err := qbt.GetTorrentTrackersCtx(ctx, torrent.Hash) + if err == nil { + trackers = loaded + } + } + out := make([]client.DownloadTaskTracker, 0, min(len(trackers), 20)) + for _, tracker := range trackers { + peers := int64(tracker.NumPeers) + seeds := int64(tracker.NumSeeds) + leechers := int64(tracker.NumLeechers) + out = append(out, client.DownloadTaskTracker{ + URL: tracker.Url, + Status: fmt.Sprint(tracker.Status), + Peers: &peers, + Seeds: &seeds, + Leechers: &leechers, + Message: tracker.Message, + }) + if len(out) >= 20 { + break + } + } + return out +} + +func qbittorrentPeers(ctx context.Context, qbt *qbittorrent.Client, hash string) []client.DownloadTaskPeer { + if hash == "" { + return nil + } + peers, err := qbt.GetTorrentPeersCtx(ctx, hash, 0) + if err != nil || peers == nil { + return nil + } + out := make([]client.DownloadTaskPeer, 0, min(len(peers.Peers), 20)) + for address, peer := range peers.Peers { + progress := peer.Progress + down := peer.DownSpeed + up := peer.UpSpeed + label := address + if peer.IP != "" && peer.Port > 0 { + label = fmt.Sprintf("%s:%d", peer.IP, peer.Port) + } + out = append(out, client.DownloadTaskPeer{ + Address: label, + Client: peer.Client, + Progress: &progress, + DownloadBps: &down, + UploadBps: &up, + }) + if len(out) >= 20 { + break + } + } + return out +} + func resultFromAria2Files(task client.DownloadTask, taskDir string, files []arigo.File) (Result, error) { paths := make([]string, 0, len(files)) for _, file := range files { diff --git a/downloader/internal/engine/engine_test.go b/downloader/internal/engine/engine_test.go index f3301d47..eb739d8c 100644 --- a/downloader/internal/engine/engine_test.go +++ b/downloader/internal/engine/engine_test.go @@ -23,11 +23,13 @@ func TestHTTPDownload(t *testing.T) { dir := t.TempDir() progressCalls := 0 + var lastDetail *client.DownloadTaskDetail result, err := (HTTP{Dir: dir}).Download( context.Background(), client.DownloadTask{ID: "task-1", SourceType: "http", SourceURI: server.URL + "/file.txt"}, - func(downloaded int64, total *int64, bps int64) error { + func(downloaded int64, total *int64, bps int64, detail *client.DownloadTaskDetail) error { progressCalls++ + lastDetail = detail if downloaded < 0 { t.Fatalf("downloaded bytes must not be negative") } @@ -53,13 +55,16 @@ func TestHTTPDownload(t *testing.T) { if progressCalls == 0 { t.Fatal("expected progress callback") } + if lastDetail == nil || lastDetail.Engine != "builtin" { + t.Fatalf("expected builtin progress detail, got %#v", lastDetail) + } } func TestHTTPRejectsMagnet(t *testing.T) { _, err := (HTTP{Dir: t.TempDir()}).Download( context.Background(), client.DownloadTask{ID: "task-1", SourceType: "magnet", SourceURI: "magnet:?xt=urn:btih:abc"}, - func(downloaded int64, total *int64, bps int64) error { return nil }, + func(downloaded int64, total *int64, bps int64, detail *client.DownloadTaskDetail) error { return nil }, ) if err == nil { t.Fatal("expected magnet to be rejected by HTTP engine") @@ -90,7 +95,7 @@ func TestQBittorrentHTTPDelegatesToBuiltin(t *testing.T) { result, err := (QBittorrent{URL: "http://127.0.0.1:1", Dir: t.TempDir()}).Download( context.Background(), client.DownloadTask{ID: "task-1", SourceType: "http", SourceURI: server.URL + "/file.txt"}, - func(downloaded int64, total *int64, bps int64) error { return nil }, + func(downloaded int64, total *int64, bps int64, detail *client.DownloadTaskDetail) error { return nil }, ) if err != nil { t.Fatal(err) diff --git a/downloader/internal/engine/live_download_test.go b/downloader/internal/engine/live_download_test.go index 4b518f58..622a2b18 100644 --- a/downloader/internal/engine/live_download_test.go +++ b/downloader/internal/engine/live_download_test.go @@ -246,7 +246,7 @@ func TestLiveQBittorrentDownloadTorrentURL(t *testing.T) { func runLiveDownload(t *testing.T, ctx context.Context, downloader Engine, task client.DownloadTask) Result { t.Helper() var lastDownloaded int64 - result, err := downloader.Download(ctx, task, func(downloaded int64, total *int64, bps int64) error { + result, err := downloader.Download(ctx, task, func(downloaded int64, total *int64, bps int64, detail *client.DownloadTaskDetail) error { if downloaded < lastDownloaded { t.Fatalf("download progress moved backwards: %d < %d", downloaded, lastDownloaded) } diff --git a/downloader/internal/openapi/client.gen.go b/downloader/internal/openapi/client.gen.go index 901e56b5..80d53c1c 100644 --- a/downloader/internal/openapi/client.gen.go +++ b/downloader/internal/openapi/client.gen.go @@ -229,6 +229,27 @@ func (e GetApiDownloadTasksParamsAssignedTo) Valid() bool { } } +// Defines values for GetApiDownloadTasks200JSONResponseBodyItemsDetailEngine. +const ( + GetApiDownloadTasks200JSONResponseBodyItemsDetailEngineAria2 GetApiDownloadTasks200JSONResponseBodyItemsDetailEngine = "aria2" + GetApiDownloadTasks200JSONResponseBodyItemsDetailEngineBuiltin GetApiDownloadTasks200JSONResponseBodyItemsDetailEngine = "builtin" + GetApiDownloadTasks200JSONResponseBodyItemsDetailEngineQbittorrent GetApiDownloadTasks200JSONResponseBodyItemsDetailEngine = "qbittorrent" +) + +// Valid indicates whether the value is a known member of the GetApiDownloadTasks200JSONResponseBodyItemsDetailEngine enum. +func (e GetApiDownloadTasks200JSONResponseBodyItemsDetailEngine) Valid() bool { + switch e { + case GetApiDownloadTasks200JSONResponseBodyItemsDetailEngineAria2: + return true + case GetApiDownloadTasks200JSONResponseBodyItemsDetailEngineBuiltin: + return true + case GetApiDownloadTasks200JSONResponseBodyItemsDetailEngineQbittorrent: + return true + default: + return false + } +} + // Defines values for GetApiDownloadTasks200JSONResponseBodyItemsSourceType. const ( GetApiDownloadTasks200JSONResponseBodyItemsSourceTypeHttp GetApiDownloadTasks200JSONResponseBodyItemsSourceType = "http" @@ -307,6 +328,27 @@ func (e PostApiDownloadTasksJSONBodySourceType) Valid() bool { } } +// Defines values for PostApiDownloadTasks201JSONResponseBodyDetailEngine. +const ( + PostApiDownloadTasks201JSONResponseBodyDetailEngineAria2 PostApiDownloadTasks201JSONResponseBodyDetailEngine = "aria2" + PostApiDownloadTasks201JSONResponseBodyDetailEngineBuiltin PostApiDownloadTasks201JSONResponseBodyDetailEngine = "builtin" + PostApiDownloadTasks201JSONResponseBodyDetailEngineQbittorrent PostApiDownloadTasks201JSONResponseBodyDetailEngine = "qbittorrent" +) + +// Valid indicates whether the value is a known member of the PostApiDownloadTasks201JSONResponseBodyDetailEngine enum. +func (e PostApiDownloadTasks201JSONResponseBodyDetailEngine) Valid() bool { + switch e { + case PostApiDownloadTasks201JSONResponseBodyDetailEngineAria2: + return true + case PostApiDownloadTasks201JSONResponseBodyDetailEngineBuiltin: + return true + case PostApiDownloadTasks201JSONResponseBodyDetailEngineQbittorrent: + return true + default: + return false + } +} + // Defines values for PostApiDownloadTasks201JSONResponseBodySourceType. const ( PostApiDownloadTasks201JSONResponseBodySourceTypeHttp PostApiDownloadTasks201JSONResponseBodySourceType = "http" @@ -364,6 +406,27 @@ func (e PostApiDownloadTasks201JSONResponseBodyStatus) Valid() bool { } } +// Defines values for GetApiDownloadTasksId200JSONResponseBodyDetailEngine. +const ( + GetApiDownloadTasksId200JSONResponseBodyDetailEngineAria2 GetApiDownloadTasksId200JSONResponseBodyDetailEngine = "aria2" + GetApiDownloadTasksId200JSONResponseBodyDetailEngineBuiltin GetApiDownloadTasksId200JSONResponseBodyDetailEngine = "builtin" + GetApiDownloadTasksId200JSONResponseBodyDetailEngineQbittorrent GetApiDownloadTasksId200JSONResponseBodyDetailEngine = "qbittorrent" +) + +// Valid indicates whether the value is a known member of the GetApiDownloadTasksId200JSONResponseBodyDetailEngine enum. +func (e GetApiDownloadTasksId200JSONResponseBodyDetailEngine) Valid() bool { + switch e { + case GetApiDownloadTasksId200JSONResponseBodyDetailEngineAria2: + return true + case GetApiDownloadTasksId200JSONResponseBodyDetailEngineBuiltin: + return true + case GetApiDownloadTasksId200JSONResponseBodyDetailEngineQbittorrent: + return true + default: + return false + } +} + // Defines values for GetApiDownloadTasksId200JSONResponseBodySourceType. const ( GetApiDownloadTasksId200JSONResponseBodySourceTypeHttp GetApiDownloadTasksId200JSONResponseBodySourceType = "http" @@ -421,6 +484,27 @@ func (e GetApiDownloadTasksId200JSONResponseBodyStatus) Valid() bool { } } +// Defines values for PatchApiDownloadTasksIdJSONBodyDetailEngine. +const ( + PatchApiDownloadTasksIdJSONBodyDetailEngineAria2 PatchApiDownloadTasksIdJSONBodyDetailEngine = "aria2" + PatchApiDownloadTasksIdJSONBodyDetailEngineBuiltin PatchApiDownloadTasksIdJSONBodyDetailEngine = "builtin" + PatchApiDownloadTasksIdJSONBodyDetailEngineQbittorrent PatchApiDownloadTasksIdJSONBodyDetailEngine = "qbittorrent" +) + +// Valid indicates whether the value is a known member of the PatchApiDownloadTasksIdJSONBodyDetailEngine enum. +func (e PatchApiDownloadTasksIdJSONBodyDetailEngine) Valid() bool { + switch e { + case PatchApiDownloadTasksIdJSONBodyDetailEngineAria2: + return true + case PatchApiDownloadTasksIdJSONBodyDetailEngineBuiltin: + return true + case PatchApiDownloadTasksIdJSONBodyDetailEngineQbittorrent: + return true + default: + return false + } +} + // Defines values for PatchApiDownloadTasksIdJSONBodyStatus. const ( PatchApiDownloadTasksIdJSONBodyStatusAssigned PatchApiDownloadTasksIdJSONBodyStatus = "assigned" @@ -457,6 +541,27 @@ func (e PatchApiDownloadTasksIdJSONBodyStatus) Valid() bool { } } +// Defines values for PatchApiDownloadTasksId200JSONResponseBodyDetailEngine. +const ( + PatchApiDownloadTasksId200JSONResponseBodyDetailEngineAria2 PatchApiDownloadTasksId200JSONResponseBodyDetailEngine = "aria2" + PatchApiDownloadTasksId200JSONResponseBodyDetailEngineBuiltin PatchApiDownloadTasksId200JSONResponseBodyDetailEngine = "builtin" + PatchApiDownloadTasksId200JSONResponseBodyDetailEngineQbittorrent PatchApiDownloadTasksId200JSONResponseBodyDetailEngine = "qbittorrent" +) + +// Valid indicates whether the value is a known member of the PatchApiDownloadTasksId200JSONResponseBodyDetailEngine enum. +func (e PatchApiDownloadTasksId200JSONResponseBodyDetailEngine) Valid() bool { + switch e { + case PatchApiDownloadTasksId200JSONResponseBodyDetailEngineAria2: + return true + case PatchApiDownloadTasksId200JSONResponseBodyDetailEngineBuiltin: + return true + case PatchApiDownloadTasksId200JSONResponseBodyDetailEngineQbittorrent: + return true + default: + return false + } +} + // Defines values for PatchApiDownloadTasksId200JSONResponseBodySourceType. const ( Http PatchApiDownloadTasksId200JSONResponseBodySourceType = "http" @@ -537,19 +642,19 @@ func (e PostApiDownloaderHeartbeatJSONBodyEngine) Valid() bool { // Defines values for PostApiDownloaderHeartbeat200JSONResponseBodyHeartbeatEngine. const ( - PostApiDownloaderHeartbeat200JSONResponseBodyHeartbeatEngineAria2 PostApiDownloaderHeartbeat200JSONResponseBodyHeartbeatEngine = "aria2" - PostApiDownloaderHeartbeat200JSONResponseBodyHeartbeatEngineBuiltin PostApiDownloaderHeartbeat200JSONResponseBodyHeartbeatEngine = "builtin" - PostApiDownloaderHeartbeat200JSONResponseBodyHeartbeatEngineQbittorrent PostApiDownloaderHeartbeat200JSONResponseBodyHeartbeatEngine = "qbittorrent" + Aria2 PostApiDownloaderHeartbeat200JSONResponseBodyHeartbeatEngine = "aria2" + Builtin PostApiDownloaderHeartbeat200JSONResponseBodyHeartbeatEngine = "builtin" + Qbittorrent PostApiDownloaderHeartbeat200JSONResponseBodyHeartbeatEngine = "qbittorrent" ) // Valid indicates whether the value is a known member of the PostApiDownloaderHeartbeat200JSONResponseBodyHeartbeatEngine enum. func (e PostApiDownloaderHeartbeat200JSONResponseBodyHeartbeatEngine) Valid() bool { switch e { - case PostApiDownloaderHeartbeat200JSONResponseBodyHeartbeatEngineAria2: + case Aria2: return true - case PostApiDownloaderHeartbeat200JSONResponseBodyHeartbeatEngineBuiltin: + case Builtin: return true - case PostApiDownloaderHeartbeat200JSONResponseBodyHeartbeatEngineQbittorrent: + case Qbittorrent: return true default: return false @@ -711,6 +816,9 @@ type GetApiDownloadTasksParamsStatus string // GetApiDownloadTasksParamsAssignedTo defines parameters for GetApiDownloadTasks. type GetApiDownloadTasksParamsAssignedTo string +// GetApiDownloadTasks200JSONResponseBodyItemsDetailEngine defines parameters for GetApiDownloadTasks. +type GetApiDownloadTasks200JSONResponseBodyItemsDetailEngine string + // GetApiDownloadTasks200JSONResponseBodyItemsSourceType defines parameters for GetApiDownloadTasks. type GetApiDownloadTasks200JSONResponseBodyItemsSourceType string @@ -730,12 +838,18 @@ type PostApiDownloadTasksJSONBody struct { // PostApiDownloadTasksJSONBodySourceType defines parameters for PostApiDownloadTasks. type PostApiDownloadTasksJSONBodySourceType string +// PostApiDownloadTasks201JSONResponseBodyDetailEngine defines parameters for PostApiDownloadTasks. +type PostApiDownloadTasks201JSONResponseBodyDetailEngine string + // PostApiDownloadTasks201JSONResponseBodySourceType defines parameters for PostApiDownloadTasks. type PostApiDownloadTasks201JSONResponseBodySourceType string // PostApiDownloadTasks201JSONResponseBodyStatus defines parameters for PostApiDownloadTasks. type PostApiDownloadTasks201JSONResponseBodyStatus string +// GetApiDownloadTasksId200JSONResponseBodyDetailEngine defines parameters for GetApiDownloadTasksId. +type GetApiDownloadTasksId200JSONResponseBodyDetailEngine string + // GetApiDownloadTasksId200JSONResponseBodySourceType defines parameters for GetApiDownloadTasksId. type GetApiDownloadTasksId200JSONResponseBodySourceType string @@ -744,6 +858,40 @@ type GetApiDownloadTasksId200JSONResponseBodyStatus string // PatchApiDownloadTasksIdJSONBody defines parameters for PatchApiDownloadTasksId. type PatchApiDownloadTasksIdJSONBody struct { + Detail *struct { + Connections *int `json:"connections,omitempty"` + Engine *PatchApiDownloadTasksIdJSONBodyDetailEngine `json:"engine,omitempty"` + EtaSeconds *int `json:"etaSeconds,omitempty"` + Files *[]struct { + CompletedBytes *int `json:"completedBytes,omitempty"` + Path string `json:"path"` + Selected *bool `json:"selected,omitempty"` + Size int `json:"size"` + } `json:"files,omitempty"` + InfoHash *string `json:"infoHash,omitempty"` + Leechers *int `json:"leechers,omitempty"` + Message *string `json:"message,omitempty"` + PeerSamples *[]struct { + Address string `json:"address"` + Client *string `json:"client,omitempty"` + DownloadBps *int `json:"downloadBps,omitempty"` + Progress *float32 `json:"progress,omitempty"` + UploadBps *int `json:"uploadBps,omitempty"` + } `json:"peerSamples,omitempty"` + Peers *int `json:"peers,omitempty"` + Phase *string `json:"phase,omitempty"` + Seeders *int `json:"seeders,omitempty"` + TorrentName *string `json:"torrentName,omitempty"` + Trackers *[]struct { + Leechers *int `json:"leechers,omitempty"` + Message *string `json:"message,omitempty"` + Peers *int `json:"peers,omitempty"` + Seeds *int `json:"seeds,omitempty"` + Status *string `json:"status,omitempty"` + Url string `json:"url"` + } `json:"trackers,omitempty"` + UploadedBytes *int `json:"uploadedBytes,omitempty"` + } `json:"detail,omitempty"` DownloadBps *int `json:"downloadBps,omitempty"` DownloadedBytes *int `json:"downloadedBytes,omitempty"` ErrorMessage *string `json:"errorMessage,omitempty"` @@ -753,9 +901,15 @@ type PatchApiDownloadTasksIdJSONBody struct { UploadBps *int `json:"uploadBps,omitempty"` } +// PatchApiDownloadTasksIdJSONBodyDetailEngine defines parameters for PatchApiDownloadTasksId. +type PatchApiDownloadTasksIdJSONBodyDetailEngine string + // PatchApiDownloadTasksIdJSONBodyStatus defines parameters for PatchApiDownloadTasksId. type PatchApiDownloadTasksIdJSONBodyStatus string +// PatchApiDownloadTasksId200JSONResponseBodyDetailEngine defines parameters for PatchApiDownloadTasksId. +type PatchApiDownloadTasksId200JSONResponseBodyDetailEngine string + // PatchApiDownloadTasksId200JSONResponseBodySourceType defines parameters for PatchApiDownloadTasksId. type PatchApiDownloadTasksId200JSONResponseBodySourceType string @@ -2149,19 +2303,54 @@ type GetApiDownloadTasksResponse struct { HTTPResponse *http.Response JSON200 *struct { Items []struct { - AssignedDownloaderId *string `json:"assignedDownloaderId,omitempty"` - DownloadBps int64 `json:"downloadBps"` - DownloadedBytes int64 `json:"downloadedBytes"` - Id string `json:"id"` - Name string `json:"name"` - ResultObjectId *string `json:"resultObjectId,omitempty"` - SourceType GetApiDownloadTasks200JSONResponseBodyItemsSourceType `json:"sourceType"` - SourceUri string `json:"sourceUri"` - Status GetApiDownloadTasks200JSONResponseBodyItemsStatus `json:"status"` - TargetFolder string `json:"targetFolder"` - TotalBytes *int64 `json:"totalBytes"` - UploadBps int64 `json:"uploadBps"` - UploadToken *string `json:"uploadToken,omitempty"` + AssignedDownloaderId *string `json:"assignedDownloaderId,omitempty"` + Detail *struct { + Connections *int `json:"connections,omitempty"` + Engine *GetApiDownloadTasks200JSONResponseBodyItemsDetailEngine `json:"engine,omitempty"` + EtaSeconds *int `json:"etaSeconds,omitempty"` + Files *[]struct { + CompletedBytes *int `json:"completedBytes,omitempty"` + Path string `json:"path"` + Selected *bool `json:"selected,omitempty"` + Size int `json:"size"` + } `json:"files,omitempty"` + InfoHash *string `json:"infoHash,omitempty"` + Leechers *int `json:"leechers,omitempty"` + Message *string `json:"message,omitempty"` + PeerSamples *[]struct { + Address string `json:"address"` + Client *string `json:"client,omitempty"` + DownloadBps *int `json:"downloadBps,omitempty"` + Progress *float32 `json:"progress,omitempty"` + UploadBps *int `json:"uploadBps,omitempty"` + } `json:"peerSamples,omitempty"` + Peers *int `json:"peers,omitempty"` + Phase *string `json:"phase,omitempty"` + Seeders *int `json:"seeders,omitempty"` + TorrentName *string `json:"torrentName,omitempty"` + Trackers *[]struct { + Leechers *int `json:"leechers,omitempty"` + Message *string `json:"message,omitempty"` + Peers *int `json:"peers,omitempty"` + Seeds *int `json:"seeds,omitempty"` + Status *string `json:"status,omitempty"` + Url string `json:"url"` + } `json:"trackers,omitempty"` + UploadedBytes *int `json:"uploadedBytes,omitempty"` + } `json:"detail,omitempty"` + DownloadBps int64 `json:"downloadBps"` + DownloadedBytes int64 `json:"downloadedBytes"` + ErrorMessage *string `json:"errorMessage,omitempty"` + Id string `json:"id"` + Name string `json:"name"` + ResultObjectId *string `json:"resultObjectId,omitempty"` + SourceType GetApiDownloadTasks200JSONResponseBodyItemsSourceType `json:"sourceType"` + SourceUri string `json:"sourceUri"` + Status GetApiDownloadTasks200JSONResponseBodyItemsStatus `json:"status"` + TargetFolder string `json:"targetFolder"` + TotalBytes *int64 `json:"totalBytes"` + UploadBps int64 `json:"uploadBps"` + UploadToken *string `json:"uploadToken,omitempty"` } `json:"items"` Page int `json:"page"` PageSize int `json:"pageSize"` @@ -2200,19 +2389,54 @@ type PostApiDownloadTasksResponse struct { Body []byte HTTPResponse *http.Response JSON201 *struct { - AssignedDownloaderId *string `json:"assignedDownloaderId,omitempty"` - DownloadBps int64 `json:"downloadBps"` - DownloadedBytes int64 `json:"downloadedBytes"` - Id string `json:"id"` - Name string `json:"name"` - ResultObjectId *string `json:"resultObjectId,omitempty"` - SourceType PostApiDownloadTasks201JSONResponseBodySourceType `json:"sourceType"` - SourceUri string `json:"sourceUri"` - Status PostApiDownloadTasks201JSONResponseBodyStatus `json:"status"` - TargetFolder string `json:"targetFolder"` - TotalBytes *int64 `json:"totalBytes"` - UploadBps int64 `json:"uploadBps"` - UploadToken *string `json:"uploadToken,omitempty"` + AssignedDownloaderId *string `json:"assignedDownloaderId,omitempty"` + Detail *struct { + Connections *int `json:"connections,omitempty"` + Engine *PostApiDownloadTasks201JSONResponseBodyDetailEngine `json:"engine,omitempty"` + EtaSeconds *int `json:"etaSeconds,omitempty"` + Files *[]struct { + CompletedBytes *int `json:"completedBytes,omitempty"` + Path string `json:"path"` + Selected *bool `json:"selected,omitempty"` + Size int `json:"size"` + } `json:"files,omitempty"` + InfoHash *string `json:"infoHash,omitempty"` + Leechers *int `json:"leechers,omitempty"` + Message *string `json:"message,omitempty"` + PeerSamples *[]struct { + Address string `json:"address"` + Client *string `json:"client,omitempty"` + DownloadBps *int `json:"downloadBps,omitempty"` + Progress *float32 `json:"progress,omitempty"` + UploadBps *int `json:"uploadBps,omitempty"` + } `json:"peerSamples,omitempty"` + Peers *int `json:"peers,omitempty"` + Phase *string `json:"phase,omitempty"` + Seeders *int `json:"seeders,omitempty"` + TorrentName *string `json:"torrentName,omitempty"` + Trackers *[]struct { + Leechers *int `json:"leechers,omitempty"` + Message *string `json:"message,omitempty"` + Peers *int `json:"peers,omitempty"` + Seeds *int `json:"seeds,omitempty"` + Status *string `json:"status,omitempty"` + Url string `json:"url"` + } `json:"trackers,omitempty"` + UploadedBytes *int `json:"uploadedBytes,omitempty"` + } `json:"detail,omitempty"` + DownloadBps int64 `json:"downloadBps"` + DownloadedBytes int64 `json:"downloadedBytes"` + ErrorMessage *string `json:"errorMessage,omitempty"` + Id string `json:"id"` + Name string `json:"name"` + ResultObjectId *string `json:"resultObjectId,omitempty"` + SourceType PostApiDownloadTasks201JSONResponseBodySourceType `json:"sourceType"` + SourceUri string `json:"sourceUri"` + Status PostApiDownloadTasks201JSONResponseBodyStatus `json:"status"` + TargetFolder string `json:"targetFolder"` + TotalBytes *int64 `json:"totalBytes"` + UploadBps int64 `json:"uploadBps"` + UploadToken *string `json:"uploadToken,omitempty"` } JSON401 *struct { Error string `json:"error"` @@ -2253,19 +2477,54 @@ type GetApiDownloadTasksIdResponse struct { Body []byte HTTPResponse *http.Response JSON200 *struct { - AssignedDownloaderId *string `json:"assignedDownloaderId,omitempty"` - DownloadBps int64 `json:"downloadBps"` - DownloadedBytes int64 `json:"downloadedBytes"` - Id string `json:"id"` - Name string `json:"name"` - ResultObjectId *string `json:"resultObjectId,omitempty"` - SourceType GetApiDownloadTasksId200JSONResponseBodySourceType `json:"sourceType"` - SourceUri string `json:"sourceUri"` - Status GetApiDownloadTasksId200JSONResponseBodyStatus `json:"status"` - TargetFolder string `json:"targetFolder"` - TotalBytes *int64 `json:"totalBytes"` - UploadBps int64 `json:"uploadBps"` - UploadToken *string `json:"uploadToken,omitempty"` + AssignedDownloaderId *string `json:"assignedDownloaderId,omitempty"` + Detail *struct { + Connections *int `json:"connections,omitempty"` + Engine *GetApiDownloadTasksId200JSONResponseBodyDetailEngine `json:"engine,omitempty"` + EtaSeconds *int `json:"etaSeconds,omitempty"` + Files *[]struct { + CompletedBytes *int `json:"completedBytes,omitempty"` + Path string `json:"path"` + Selected *bool `json:"selected,omitempty"` + Size int `json:"size"` + } `json:"files,omitempty"` + InfoHash *string `json:"infoHash,omitempty"` + Leechers *int `json:"leechers,omitempty"` + Message *string `json:"message,omitempty"` + PeerSamples *[]struct { + Address string `json:"address"` + Client *string `json:"client,omitempty"` + DownloadBps *int `json:"downloadBps,omitempty"` + Progress *float32 `json:"progress,omitempty"` + UploadBps *int `json:"uploadBps,omitempty"` + } `json:"peerSamples,omitempty"` + Peers *int `json:"peers,omitempty"` + Phase *string `json:"phase,omitempty"` + Seeders *int `json:"seeders,omitempty"` + TorrentName *string `json:"torrentName,omitempty"` + Trackers *[]struct { + Leechers *int `json:"leechers,omitempty"` + Message *string `json:"message,omitempty"` + Peers *int `json:"peers,omitempty"` + Seeds *int `json:"seeds,omitempty"` + Status *string `json:"status,omitempty"` + Url string `json:"url"` + } `json:"trackers,omitempty"` + UploadedBytes *int `json:"uploadedBytes,omitempty"` + } `json:"detail,omitempty"` + DownloadBps int64 `json:"downloadBps"` + DownloadedBytes int64 `json:"downloadedBytes"` + ErrorMessage *string `json:"errorMessage,omitempty"` + Id string `json:"id"` + Name string `json:"name"` + ResultObjectId *string `json:"resultObjectId,omitempty"` + SourceType GetApiDownloadTasksId200JSONResponseBodySourceType `json:"sourceType"` + SourceUri string `json:"sourceUri"` + Status GetApiDownloadTasksId200JSONResponseBodyStatus `json:"status"` + TargetFolder string `json:"targetFolder"` + TotalBytes *int64 `json:"totalBytes"` + UploadBps int64 `json:"uploadBps"` + UploadToken *string `json:"uploadToken,omitempty"` } JSON404 *struct { Error string `json:"error"` @@ -2300,19 +2559,54 @@ type PatchApiDownloadTasksIdResponse struct { Body []byte HTTPResponse *http.Response JSON200 *struct { - AssignedDownloaderId *string `json:"assignedDownloaderId,omitempty"` - DownloadBps int64 `json:"downloadBps"` - DownloadedBytes int64 `json:"downloadedBytes"` - Id string `json:"id"` - Name string `json:"name"` - ResultObjectId *string `json:"resultObjectId,omitempty"` - SourceType PatchApiDownloadTasksId200JSONResponseBodySourceType `json:"sourceType"` - SourceUri string `json:"sourceUri"` - Status PatchApiDownloadTasksId200JSONResponseBodyStatus `json:"status"` - TargetFolder string `json:"targetFolder"` - TotalBytes *int64 `json:"totalBytes"` - UploadBps int64 `json:"uploadBps"` - UploadToken *string `json:"uploadToken,omitempty"` + AssignedDownloaderId *string `json:"assignedDownloaderId,omitempty"` + Detail *struct { + Connections *int `json:"connections,omitempty"` + Engine *PatchApiDownloadTasksId200JSONResponseBodyDetailEngine `json:"engine,omitempty"` + EtaSeconds *int `json:"etaSeconds,omitempty"` + Files *[]struct { + CompletedBytes *int `json:"completedBytes,omitempty"` + Path string `json:"path"` + Selected *bool `json:"selected,omitempty"` + Size int `json:"size"` + } `json:"files,omitempty"` + InfoHash *string `json:"infoHash,omitempty"` + Leechers *int `json:"leechers,omitempty"` + Message *string `json:"message,omitempty"` + PeerSamples *[]struct { + Address string `json:"address"` + Client *string `json:"client,omitempty"` + DownloadBps *int `json:"downloadBps,omitempty"` + Progress *float32 `json:"progress,omitempty"` + UploadBps *int `json:"uploadBps,omitempty"` + } `json:"peerSamples,omitempty"` + Peers *int `json:"peers,omitempty"` + Phase *string `json:"phase,omitempty"` + Seeders *int `json:"seeders,omitempty"` + TorrentName *string `json:"torrentName,omitempty"` + Trackers *[]struct { + Leechers *int `json:"leechers,omitempty"` + Message *string `json:"message,omitempty"` + Peers *int `json:"peers,omitempty"` + Seeds *int `json:"seeds,omitempty"` + Status *string `json:"status,omitempty"` + Url string `json:"url"` + } `json:"trackers,omitempty"` + UploadedBytes *int `json:"uploadedBytes,omitempty"` + } `json:"detail,omitempty"` + DownloadBps int64 `json:"downloadBps"` + DownloadedBytes int64 `json:"downloadedBytes"` + ErrorMessage *string `json:"errorMessage,omitempty"` + Id string `json:"id"` + Name string `json:"name"` + ResultObjectId *string `json:"resultObjectId,omitempty"` + SourceType PatchApiDownloadTasksId200JSONResponseBodySourceType `json:"sourceType"` + SourceUri string `json:"sourceUri"` + Status PatchApiDownloadTasksId200JSONResponseBodyStatus `json:"status"` + TargetFolder string `json:"targetFolder"` + TotalBytes *int64 `json:"totalBytes"` + UploadBps int64 `json:"uploadBps"` + UploadToken *string `json:"uploadToken,omitempty"` } JSON401 *struct { Error string `json:"error"` @@ -2936,19 +3230,54 @@ func ParseGetApiDownloadTasksResponse(rsp *http.Response) (*GetApiDownloadTasksR case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: var dest struct { Items []struct { - AssignedDownloaderId *string `json:"assignedDownloaderId,omitempty"` - DownloadBps int64 `json:"downloadBps"` - DownloadedBytes int64 `json:"downloadedBytes"` - Id string `json:"id"` - Name string `json:"name"` - ResultObjectId *string `json:"resultObjectId,omitempty"` - SourceType GetApiDownloadTasks200JSONResponseBodyItemsSourceType `json:"sourceType"` - SourceUri string `json:"sourceUri"` - Status GetApiDownloadTasks200JSONResponseBodyItemsStatus `json:"status"` - TargetFolder string `json:"targetFolder"` - TotalBytes *int64 `json:"totalBytes"` - UploadBps int64 `json:"uploadBps"` - UploadToken *string `json:"uploadToken,omitempty"` + AssignedDownloaderId *string `json:"assignedDownloaderId,omitempty"` + Detail *struct { + Connections *int `json:"connections,omitempty"` + Engine *GetApiDownloadTasks200JSONResponseBodyItemsDetailEngine `json:"engine,omitempty"` + EtaSeconds *int `json:"etaSeconds,omitempty"` + Files *[]struct { + CompletedBytes *int `json:"completedBytes,omitempty"` + Path string `json:"path"` + Selected *bool `json:"selected,omitempty"` + Size int `json:"size"` + } `json:"files,omitempty"` + InfoHash *string `json:"infoHash,omitempty"` + Leechers *int `json:"leechers,omitempty"` + Message *string `json:"message,omitempty"` + PeerSamples *[]struct { + Address string `json:"address"` + Client *string `json:"client,omitempty"` + DownloadBps *int `json:"downloadBps,omitempty"` + Progress *float32 `json:"progress,omitempty"` + UploadBps *int `json:"uploadBps,omitempty"` + } `json:"peerSamples,omitempty"` + Peers *int `json:"peers,omitempty"` + Phase *string `json:"phase,omitempty"` + Seeders *int `json:"seeders,omitempty"` + TorrentName *string `json:"torrentName,omitempty"` + Trackers *[]struct { + Leechers *int `json:"leechers,omitempty"` + Message *string `json:"message,omitempty"` + Peers *int `json:"peers,omitempty"` + Seeds *int `json:"seeds,omitempty"` + Status *string `json:"status,omitempty"` + Url string `json:"url"` + } `json:"trackers,omitempty"` + UploadedBytes *int `json:"uploadedBytes,omitempty"` + } `json:"detail,omitempty"` + DownloadBps int64 `json:"downloadBps"` + DownloadedBytes int64 `json:"downloadedBytes"` + ErrorMessage *string `json:"errorMessage,omitempty"` + Id string `json:"id"` + Name string `json:"name"` + ResultObjectId *string `json:"resultObjectId,omitempty"` + SourceType GetApiDownloadTasks200JSONResponseBodyItemsSourceType `json:"sourceType"` + SourceUri string `json:"sourceUri"` + Status GetApiDownloadTasks200JSONResponseBodyItemsStatus `json:"status"` + TargetFolder string `json:"targetFolder"` + TotalBytes *int64 `json:"totalBytes"` + UploadBps int64 `json:"uploadBps"` + UploadToken *string `json:"uploadToken,omitempty"` } `json:"items"` Page int `json:"page"` PageSize int `json:"pageSize"` @@ -2989,19 +3318,54 @@ func ParsePostApiDownloadTasksResponse(rsp *http.Response) (*PostApiDownloadTask switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: var dest struct { - AssignedDownloaderId *string `json:"assignedDownloaderId,omitempty"` - DownloadBps int64 `json:"downloadBps"` - DownloadedBytes int64 `json:"downloadedBytes"` - Id string `json:"id"` - Name string `json:"name"` - ResultObjectId *string `json:"resultObjectId,omitempty"` - SourceType PostApiDownloadTasks201JSONResponseBodySourceType `json:"sourceType"` - SourceUri string `json:"sourceUri"` - Status PostApiDownloadTasks201JSONResponseBodyStatus `json:"status"` - TargetFolder string `json:"targetFolder"` - TotalBytes *int64 `json:"totalBytes"` - UploadBps int64 `json:"uploadBps"` - UploadToken *string `json:"uploadToken,omitempty"` + AssignedDownloaderId *string `json:"assignedDownloaderId,omitempty"` + Detail *struct { + Connections *int `json:"connections,omitempty"` + Engine *PostApiDownloadTasks201JSONResponseBodyDetailEngine `json:"engine,omitempty"` + EtaSeconds *int `json:"etaSeconds,omitempty"` + Files *[]struct { + CompletedBytes *int `json:"completedBytes,omitempty"` + Path string `json:"path"` + Selected *bool `json:"selected,omitempty"` + Size int `json:"size"` + } `json:"files,omitempty"` + InfoHash *string `json:"infoHash,omitempty"` + Leechers *int `json:"leechers,omitempty"` + Message *string `json:"message,omitempty"` + PeerSamples *[]struct { + Address string `json:"address"` + Client *string `json:"client,omitempty"` + DownloadBps *int `json:"downloadBps,omitempty"` + Progress *float32 `json:"progress,omitempty"` + UploadBps *int `json:"uploadBps,omitempty"` + } `json:"peerSamples,omitempty"` + Peers *int `json:"peers,omitempty"` + Phase *string `json:"phase,omitempty"` + Seeders *int `json:"seeders,omitempty"` + TorrentName *string `json:"torrentName,omitempty"` + Trackers *[]struct { + Leechers *int `json:"leechers,omitempty"` + Message *string `json:"message,omitempty"` + Peers *int `json:"peers,omitempty"` + Seeds *int `json:"seeds,omitempty"` + Status *string `json:"status,omitempty"` + Url string `json:"url"` + } `json:"trackers,omitempty"` + UploadedBytes *int `json:"uploadedBytes,omitempty"` + } `json:"detail,omitempty"` + DownloadBps int64 `json:"downloadBps"` + DownloadedBytes int64 `json:"downloadedBytes"` + ErrorMessage *string `json:"errorMessage,omitempty"` + Id string `json:"id"` + Name string `json:"name"` + ResultObjectId *string `json:"resultObjectId,omitempty"` + SourceType PostApiDownloadTasks201JSONResponseBodySourceType `json:"sourceType"` + SourceUri string `json:"sourceUri"` + Status PostApiDownloadTasks201JSONResponseBodyStatus `json:"status"` + TargetFolder string `json:"targetFolder"` + TotalBytes *int64 `json:"totalBytes"` + UploadBps int64 `json:"uploadBps"` + UploadToken *string `json:"uploadToken,omitempty"` } if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err @@ -3056,19 +3420,54 @@ func ParseGetApiDownloadTasksIdResponse(rsp *http.Response) (*GetApiDownloadTask switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: var dest struct { - AssignedDownloaderId *string `json:"assignedDownloaderId,omitempty"` - DownloadBps int64 `json:"downloadBps"` - DownloadedBytes int64 `json:"downloadedBytes"` - Id string `json:"id"` - Name string `json:"name"` - ResultObjectId *string `json:"resultObjectId,omitempty"` - SourceType GetApiDownloadTasksId200JSONResponseBodySourceType `json:"sourceType"` - SourceUri string `json:"sourceUri"` - Status GetApiDownloadTasksId200JSONResponseBodyStatus `json:"status"` - TargetFolder string `json:"targetFolder"` - TotalBytes *int64 `json:"totalBytes"` - UploadBps int64 `json:"uploadBps"` - UploadToken *string `json:"uploadToken,omitempty"` + AssignedDownloaderId *string `json:"assignedDownloaderId,omitempty"` + Detail *struct { + Connections *int `json:"connections,omitempty"` + Engine *GetApiDownloadTasksId200JSONResponseBodyDetailEngine `json:"engine,omitempty"` + EtaSeconds *int `json:"etaSeconds,omitempty"` + Files *[]struct { + CompletedBytes *int `json:"completedBytes,omitempty"` + Path string `json:"path"` + Selected *bool `json:"selected,omitempty"` + Size int `json:"size"` + } `json:"files,omitempty"` + InfoHash *string `json:"infoHash,omitempty"` + Leechers *int `json:"leechers,omitempty"` + Message *string `json:"message,omitempty"` + PeerSamples *[]struct { + Address string `json:"address"` + Client *string `json:"client,omitempty"` + DownloadBps *int `json:"downloadBps,omitempty"` + Progress *float32 `json:"progress,omitempty"` + UploadBps *int `json:"uploadBps,omitempty"` + } `json:"peerSamples,omitempty"` + Peers *int `json:"peers,omitempty"` + Phase *string `json:"phase,omitempty"` + Seeders *int `json:"seeders,omitempty"` + TorrentName *string `json:"torrentName,omitempty"` + Trackers *[]struct { + Leechers *int `json:"leechers,omitempty"` + Message *string `json:"message,omitempty"` + Peers *int `json:"peers,omitempty"` + Seeds *int `json:"seeds,omitempty"` + Status *string `json:"status,omitempty"` + Url string `json:"url"` + } `json:"trackers,omitempty"` + UploadedBytes *int `json:"uploadedBytes,omitempty"` + } `json:"detail,omitempty"` + DownloadBps int64 `json:"downloadBps"` + DownloadedBytes int64 `json:"downloadedBytes"` + ErrorMessage *string `json:"errorMessage,omitempty"` + Id string `json:"id"` + Name string `json:"name"` + ResultObjectId *string `json:"resultObjectId,omitempty"` + SourceType GetApiDownloadTasksId200JSONResponseBodySourceType `json:"sourceType"` + SourceUri string `json:"sourceUri"` + Status GetApiDownloadTasksId200JSONResponseBodyStatus `json:"status"` + TargetFolder string `json:"targetFolder"` + TotalBytes *int64 `json:"totalBytes"` + UploadBps int64 `json:"uploadBps"` + UploadToken *string `json:"uploadToken,omitempty"` } if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err @@ -3105,19 +3504,54 @@ func ParsePatchApiDownloadTasksIdResponse(rsp *http.Response) (*PatchApiDownload switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: var dest struct { - AssignedDownloaderId *string `json:"assignedDownloaderId,omitempty"` - DownloadBps int64 `json:"downloadBps"` - DownloadedBytes int64 `json:"downloadedBytes"` - Id string `json:"id"` - Name string `json:"name"` - ResultObjectId *string `json:"resultObjectId,omitempty"` - SourceType PatchApiDownloadTasksId200JSONResponseBodySourceType `json:"sourceType"` - SourceUri string `json:"sourceUri"` - Status PatchApiDownloadTasksId200JSONResponseBodyStatus `json:"status"` - TargetFolder string `json:"targetFolder"` - TotalBytes *int64 `json:"totalBytes"` - UploadBps int64 `json:"uploadBps"` - UploadToken *string `json:"uploadToken,omitempty"` + AssignedDownloaderId *string `json:"assignedDownloaderId,omitempty"` + Detail *struct { + Connections *int `json:"connections,omitempty"` + Engine *PatchApiDownloadTasksId200JSONResponseBodyDetailEngine `json:"engine,omitempty"` + EtaSeconds *int `json:"etaSeconds,omitempty"` + Files *[]struct { + CompletedBytes *int `json:"completedBytes,omitempty"` + Path string `json:"path"` + Selected *bool `json:"selected,omitempty"` + Size int `json:"size"` + } `json:"files,omitempty"` + InfoHash *string `json:"infoHash,omitempty"` + Leechers *int `json:"leechers,omitempty"` + Message *string `json:"message,omitempty"` + PeerSamples *[]struct { + Address string `json:"address"` + Client *string `json:"client,omitempty"` + DownloadBps *int `json:"downloadBps,omitempty"` + Progress *float32 `json:"progress,omitempty"` + UploadBps *int `json:"uploadBps,omitempty"` + } `json:"peerSamples,omitempty"` + Peers *int `json:"peers,omitempty"` + Phase *string `json:"phase,omitempty"` + Seeders *int `json:"seeders,omitempty"` + TorrentName *string `json:"torrentName,omitempty"` + Trackers *[]struct { + Leechers *int `json:"leechers,omitempty"` + Message *string `json:"message,omitempty"` + Peers *int `json:"peers,omitempty"` + Seeds *int `json:"seeds,omitempty"` + Status *string `json:"status,omitempty"` + Url string `json:"url"` + } `json:"trackers,omitempty"` + UploadedBytes *int `json:"uploadedBytes,omitempty"` + } `json:"detail,omitempty"` + DownloadBps int64 `json:"downloadBps"` + DownloadedBytes int64 `json:"downloadedBytes"` + ErrorMessage *string `json:"errorMessage,omitempty"` + Id string `json:"id"` + Name string `json:"name"` + ResultObjectId *string `json:"resultObjectId,omitempty"` + SourceType PatchApiDownloadTasksId200JSONResponseBodySourceType `json:"sourceType"` + SourceUri string `json:"sourceUri"` + Status PatchApiDownloadTasksId200JSONResponseBodyStatus `json:"status"` + TargetFolder string `json:"targetFolder"` + TotalBytes *int64 `json:"totalBytes"` + UploadBps int64 `json:"uploadBps"` + UploadToken *string `json:"uploadToken,omitempty"` } if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err diff --git a/downloader/internal/worker/worker.go b/downloader/internal/worker/worker.go index fd53d3ff..7bb04c1d 100644 --- a/downloader/internal/worker/worker.go +++ b/downloader/internal/worker/worker.go @@ -122,11 +122,12 @@ func (w *Worker) process(ctx context.Context, task client.DownloadTask) { } var lastProgressLog time.Time - result, err := w.engine.Download(ctx, task, func(downloaded int64, total *int64, bps int64) error { + result, err := w.engine.Download(ctx, task, func(downloaded int64, total *int64, bps int64, detail *client.DownloadTaskDetail) error { _, updateErr := w.api.UpdateTask(ctx, task.ID, client.TaskPatch{ DownloadedBytes: &downloaded, TotalBytes: total, DownloadBps: &bps, + Detail: detail, }) if updateErr != nil && strings.Contains(updateErr.Error(), "insufficient_credits") { log.Warn("task paused by billing", "downloaded_bytes", downloaded, "total_bytes", optionalInt64(total), "bps", bps) diff --git a/migrations/0036_add-download-task-detail.sql b/migrations/0036_add-download-task-detail.sql new file mode 100644 index 00000000..982a58b8 --- /dev/null +++ b/migrations/0036_add-download-task-detail.sql @@ -0,0 +1 @@ +ALTER TABLE `download_tasks` ADD `detail` text; \ No newline at end of file diff --git a/migrations/meta/0036_snapshot.json b/migrations/meta/0036_snapshot.json new file mode 100644 index 00000000..3ac1aec9 --- /dev/null +++ b/migrations/meta/0036_snapshot.json @@ -0,0 +1,3914 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "44bd07cd-4e8d-4622-8a26-7805056a3324", + "prevId": "5b8ccfb0-4bf9-4cad-985e-9fb39284a491", + "tables": { + "activity_events": { + "name": "activity_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_name": { + "name": "target_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "announcements": { + "name": "announcements", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "announcements_status_priority_idx": { + "name": "announcements_status_priority_idx", + "columns": [ + "status", + "priority" + ], + "isUnique": false + }, + "announcements_published_idx": { + "name": "announcements_published_idx", + "columns": [ + "published_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "background_jobs": { + "name": "background_jobs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_folder": { + "name": "target_folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_path": { + "name": "target_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "input_bytes": { + "name": "input_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_bytes": { + "name": "output_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "processed_bytes": { + "name": "processed_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "file_count": { + "name": "file_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "current_filename": { + "name": "current_filename", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_metadata": { + "name": "result_metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retryable": { + "name": "retryable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "cancelable": { + "name": "cancelable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "retried_from_job_id": { + "name": "retried_from_job_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "background_jobs_org_created_idx": { + "name": "background_jobs_org_created_idx", + "columns": [ + "org_id", + "created_at" + ], + "isUnique": false + }, + "background_jobs_org_status_idx": { + "name": "background_jobs_org_status_idx", + "columns": [ + "org_id", + "status" + ], + "isUnique": false + }, + "background_jobs_org_type_idx": { + "name": "background_jobs_org_type_idx", + "columns": [ + "org_id", + "type" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cloud_traffic_reports": { + "name": "cloud_traffic_reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "period": { + "name": "period", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bytes": { + "name": "bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit_bytes": { + "name": "unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credits_per_unit": { + "name": "credits_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "cloud_traffic_reports_event_uniq": { + "name": "cloud_traffic_reports_event_uniq", + "columns": [ + "event_id" + ], + "isUnique": true + }, + "cloud_traffic_reports_org_period_idx": { + "name": "cloud_traffic_reports_org_period_idx", + "columns": [ + "org_id", + "period" + ], + "isUnique": false + }, + "cloud_traffic_reports_status_idx": { + "name": "cloud_traffic_reports_status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "download_tasks": { + "name": "download_tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_uri": { + "name": "source_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_folder": { + "name": "target_folder", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "assigned_downloader_id": { + "name": "assigned_downloader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "downloaded_bytes": { + "name": "downloaded_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total_bytes": { + "name": "total_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "authorized_bytes": { + "name": "authorized_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "billed_bytes": { + "name": "billed_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "billed_credits": { + "name": "billed_credits", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "billing_status": { + "name": "billing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "download_bps": { + "name": "download_bps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "upload_bps": { + "name": "upload_bps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_object_id": { + "name": "result_object_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "upload_token_hash": { + "name": "upload_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "upload_token_jti": { + "name": "upload_token_jti", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "upload_token_expires_at": { + "name": "upload_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "download_tasks_org_created_idx": { + "name": "download_tasks_org_created_idx", + "columns": [ + "org_id", + "created_at" + ], + "isUnique": false + }, + "download_tasks_org_status_idx": { + "name": "download_tasks_org_status_idx", + "columns": [ + "org_id", + "status" + ], + "isUnique": false + }, + "download_tasks_downloader_idx": { + "name": "download_tasks_downloader_idx", + "columns": [ + "assigned_downloader_id", + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "downloaders": { + "name": "downloaders", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_jti": { + "name": "token_jti", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'offline'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "arch": { + "name": "arch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "engine": { + "name": "engine", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'builtin'" + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "max_concurrent_tasks": { + "name": "max_concurrent_tasks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "current_tasks": { + "name": "current_tasks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "download_bps": { + "name": "download_bps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "upload_bps": { + "name": "upload_bps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "free_disk_bytes": { + "name": "free_disk_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "remote_download_credit_billing_enabled": { + "name": "remote_download_credit_billing_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remote_download_credit_unit_bytes": { + "name": "remote_download_credit_unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 104857600 + }, + "remote_download_credit_per_unit": { + "name": "remote_download_credit_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "downloaders_token_jti_unique": { + "name": "downloaders_token_jti_unique", + "columns": [ + "token_jti" + ], + "isUnique": true + }, + "downloaders_status_idx": { + "name": "downloaders_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "downloaders_enabled_idx": { + "name": "downloaders_enabled_idx", + "columns": [ + "enabled" + ], + "isUnique": false + }, + "downloaders_created_idx": { + "name": "downloaders_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "image_hosting_configs": { + "name": "image_hosting_configs", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "custom_domain": { + "name": "custom_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cf_hostname_id": { + "name": "cf_hostname_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain_verified_at": { + "name": "domain_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "referer_allowlist": { + "name": "referer_allowlist", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "image_hosting_configs_custom_domain_unique": { + "name": "image_hosting_configs_custom_domain_unique", + "columns": [ + "custom_domain" + ], + "isUnique": true + } + }, + "foreignKeys": { + "image_hosting_configs_org_id_organization_id_fk": { + "name": "image_hosting_configs_org_id_organization_id_fk", + "tableFrom": "image_hosting_configs", + "tableTo": "organization", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "image_hostings": { + "name": "image_hostings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime": { + "name": "mime", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "image_hostings_token_unique": { + "name": "image_hostings_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "image_hostings_org_path_uniq": { + "name": "image_hostings_org_path_uniq", + "columns": [ + "org_id", + "path" + ], + "isUnique": true + }, + "image_hostings_org_created_idx": { + "name": "image_hostings_org_created_idx", + "columns": [ + "org_id", + "created_at" + ], + "isUnique": false + }, + "image_hostings_token_idx": { + "name": "image_hostings_token_idx", + "columns": [ + "token" + ], + "isUnique": false + } + }, + "foreignKeys": { + "image_hostings_org_id_organization_id_fk": { + "name": "image_hostings_org_id_organization_id_fk", + "tableFrom": "image_hostings", + "tableTo": "organization", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "image_hostings_storage_id_storages_id_fk": { + "name": "image_hostings_storage_id_storages_id_fk", + "tableFrom": "image_hostings", + "tableTo": "storages", + "columnsFrom": [ + "storage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite_codes": { + "name": "invite_codes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_by": { + "name": "used_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invite_codes_code_unique": { + "name": "invite_codes_code_unique", + "columns": [ + "code" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "license_bindings": { + "name": "license_bindings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "cloud_binding_id": { + "name": "cloud_binding_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_store_id": { + "name": "cloud_store_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_account_id": { + "name": "cloud_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_account_email": { + "name": "cloud_account_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cached_certificate": { + "name": "cached_certificate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cached_certificate_expires_at": { + "name": "cached_certificate_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_at": { + "name": "bound_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_refresh_at": { + "name": "last_refresh_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_refresh_error": { + "name": "last_refresh_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "license_bindings_active_uniq": { + "name": "license_bindings_active_uniq", + "columns": [ + "status" + ], + "isUnique": true, + "where": "status = 'active'" + }, + "license_bindings_cloud_binding_idx": { + "name": "license_bindings_cloud_binding_idx", + "columns": [ + "cloud_binding_id" + ], + "isUnique": false + }, + "license_bindings_instance_idx": { + "name": "license_bindings_instance_idx", + "columns": [ + "instance_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "matters": { + "name": "matters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "dirtype": { + "name": "dirtype", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "parent": { + "name": "parent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "object": { + "name": "object", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "trashed_at": { + "name": "trashed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "matters_alias_unique": { + "name": "matters_alias_unique", + "columns": [ + "alias" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notifications": { + "name": "notifications", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "ref_type": { + "name": "ref_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ref_id": { + "name": "ref_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "read_at": { + "name": "read_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "notifications_user_created_idx": { + "name": "notifications_user_created_idx", + "columns": [ + "user_id", + "created_at" + ], + "isUnique": false + }, + "notifications_user_read_idx": { + "name": "notifications_user_read_idx", + "columns": [ + "user_id", + "read_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "object_upload_sessions": { + "name": "object_upload_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_id": { + "name": "object_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "upload_id": { + "name": "upload_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "object_upload_sessions_object_idx": { + "name": "object_upload_sessions_object_idx", + "columns": [ + "org_id", + "object_id" + ], + "isUnique": false + }, + "object_upload_sessions_expires_idx": { + "name": "object_upload_sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "org_quota_entitlements": { + "name": "org_quota_entitlements", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entitlement_type": { + "name": "entitlement_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'grant'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bytes": { + "name": "bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "starts_at": { + "name": "starts_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "org_quota_entitlements_org_resource_idx": { + "name": "org_quota_entitlements_org_resource_idx", + "columns": [ + "org_id", + "resource_type", + "status" + ], + "isUnique": false + }, + "org_quota_entitlements_org_type_idx": { + "name": "org_quota_entitlements_org_type_idx", + "columns": [ + "org_id", + "resource_type", + "entitlement_type", + "status" + ], + "isUnique": false + }, + "org_quota_entitlements_active_plan_uniq": { + "name": "org_quota_entitlements_active_plan_uniq", + "columns": [ + "org_id", + "resource_type", + "entitlement_type" + ], + "isUnique": true, + "where": "status = 'active' AND entitlement_type = 'plan'" + }, + "org_quota_entitlements_source_resource_uniq": { + "name": "org_quota_entitlements_source_resource_uniq", + "columns": [ + "source", + "source_id", + "resource_type" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "org_quotas": { + "name": "org_quotas", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "quota": { + "name": "quota", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "used": { + "name": "used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "traffic_quota": { + "name": "traffic_quota", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "traffic_used": { + "name": "traffic_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "traffic_period": { + "name": "traffic_period", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'1970-01'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "remote_download_usage_reports": { + "name": "remote_download_usage_reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "downloader_id": { + "name": "downloader_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unit_index": { + "name": "unit_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unit_bytes": { + "name": "unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credits_per_unit": { + "name": "credits_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "remote_download_usage_reports_event_id_unique": { + "name": "remote_download_usage_reports_event_id_unique", + "columns": [ + "event_id" + ], + "isUnique": true + }, + "remote_download_usage_task_unit_uniq": { + "name": "remote_download_usage_task_unit_uniq", + "columns": [ + "task_id", + "unit_index" + ], + "isUnique": true + }, + "remote_download_usage_org_idx": { + "name": "remote_download_usage_org_idx", + "columns": [ + "org_id" + ], + "isUnique": false + }, + "remote_download_usage_status_idx": { + "name": "remote_download_usage_status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "share_recipients": { + "name": "share_recipients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_email": { + "name": "recipient_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "share_recipients_share_id_idx": { + "name": "share_recipients_share_id_idx", + "columns": [ + "share_id" + ], + "isUnique": false + }, + "share_recipients_user_id_idx": { + "name": "share_recipients_user_id_idx", + "columns": [ + "recipient_user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shares": { + "name": "shares", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "matter_id": { + "name": "matter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "download_limit": { + "name": "download_limit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "downloads": { + "name": "downloads", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "shares_token_unique": { + "name": "shares_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "shares_creator_status_created_idx": { + "name": "shares_creator_status_created_idx", + "columns": [ + "creator_id", + "status", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "site_invitations": { + "name": "site_invitations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accepted_by": { + "name": "accepted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "site_invitations_token_unique": { + "name": "site_invitations_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "site_invitations_email_idx": { + "name": "site_invitations_email_idx", + "columns": [ + "email" + ], + "isUnique": false + }, + "site_invitations_created_idx": { + "name": "site_invitations_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "site_invitations_expires_idx": { + "name": "site_invitations_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "storages": { + "name": "storages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'auto'" + }, + "access_key": { + "name": "access_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secret_key": { + "name": "secret_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "custom_host": { + "name": "custom_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "egress_credit_billing_enabled": { + "name": "egress_credit_billing_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "egress_credit_unit_bytes": { + "name": "egress_credit_unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 104857600 + }, + "egress_credit_per_unit": { + "name": "egress_credit_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "used": { + "name": "used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "system_options": { + "name": "system_options", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "public": { + "name": "public", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "team_invite_links": { + "name": "team_invite_links", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "team_invite_links_token_unique": { + "name": "team_invite_links_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webdav_dead_properties": { + "name": "webdav_dead_properties", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_path": { + "name": "resource_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "webdav_dead_properties_resource_prop_uniq": { + "name": "webdav_dead_properties_resource_prop_uniq", + "columns": [ + "org_id", + "resource_path", + "namespace", + "name" + ], + "isUnique": true + }, + "webdav_dead_properties_resource_idx": { + "name": "webdav_dead_properties_resource_idx", + "columns": [ + "org_id", + "resource_path" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webdav_locks": { + "name": "webdav_locks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_path": { + "name": "resource_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "depth": { + "name": "depth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'infinity'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "webdav_locks_token_unique": { + "name": "webdav_locks_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "webdav_locks_resource_idx": { + "name": "webdav_locks_resource_idx", + "columns": [ + "org_id", + "resource_path" + ], + "isUnique": false + }, + "webdav_locks_expires_idx": { + "name": "webdav_locks_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webhook_events": { + "name": "webhook_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'cloud'" + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'order.quota_changed'" + }, + "payload_hash": { + "name": "payload_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "raw_payload": { + "name": "raw_payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "processed_at": { + "name": "processed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webhook_events_source_event_uniq": { + "name": "webhook_events_source_event_uniq", + "columns": [ + "source", + "event_id" + ], + "isUnique": true + }, + "webhook_events_source_created_idx": { + "name": "webhook_events_source_created_idx", + "columns": [ + "source", + "created_at" + ], + "isUnique": false + }, + "webhook_events_status_idx": { + "name": "webhook_events_status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apikey": { + "name": "apikey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_request": { + "name": "last_request", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "apikey_config_id_idx": { + "name": "apikey_config_id_idx", + "columns": [ + "config_id" + ], + "isUnique": false + }, + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + "reference_id" + ], + "isUnique": false + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + "key" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deviceCode": { + "name": "deviceCode", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "deviceCode_device_code_idx": { + "name": "deviceCode_device_code_idx", + "columns": [ + "device_code" + ], + "isUnique": false + }, + "deviceCode_user_code_idx": { + "name": "deviceCode_user_code_idx", + "columns": [ + "user_code" + ], + "isUnique": false + }, + "deviceCode_status_idx": { + "name": "deviceCode_status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "member": { + "name": "member", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "banned": { + "name": "banned", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "user_username_unique": { + "name": "user_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index a5109582..bd08178b 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -246,6 +246,13 @@ "when": 1780459022634, "tag": "0035_puzzling_hemingway", "breakpoints": true + }, + { + "idx": 36, + "version": "6", + "when": 1780499047848, + "tag": "0036_add-download-task-detail", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/db/schema.ts b/server/db/schema.ts index d0bbd651..0bc40097 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -348,6 +348,7 @@ export const downloadTasks = sqliteTable( uploadBps: integer('upload_bps').notNull().default(0), errorMessage: text('error_message'), resultObjectId: text('result_object_id'), + detail: text('detail'), uploadTokenHash: text('upload_token_hash'), uploadTokenJti: text('upload_token_jti'), uploadTokenExpiresAt: integer('upload_token_expires_at', { mode: 'timestamp_ms' }), diff --git a/server/routes/download-tasks.integration.test.ts b/server/routes/download-tasks.integration.test.ts index 58a57d7c..7c6cbfb1 100644 --- a/server/routes/download-tasks.integration.test.ts +++ b/server/routes/download-tasks.integration.test.ts @@ -203,9 +203,26 @@ describe('Download tasks API integration', () => { downloadedBytes: 10 * 1024 * 1024, totalBytes: 10 * 1024 * 1024, downloadBps: 512_000, + detail: { + engine: 'aria2', + phase: 'active', + infoHash: 'abc123', + torrentName: 'fixture', + connections: 8, + seeders: 3, + trackers: [{ url: 'udp://tracker.example/announce', status: 'working', seeds: 3, peers: 8 }], + peerSamples: [{ address: '127.0.0.1:6881', client: 'libtorrent', progress: 0.5, downloadBps: 128_000 }], + files: [{ path: 'fixture.txt', size: 10 * 1024 * 1024, completedBytes: 5 * 1024 * 1024 }], + }, }), }) expect(runningRes.status).toBe(200) + const runningTask = (await runningRes.json()) as { + detail: { engine: string; infoHash: string; trackers: Array<{ url: string }> } + } + expect(runningTask.detail.engine).toBe('aria2') + expect(runningTask.detail.infoHash).toBe('abc123') + expect(runningTask.detail.trackers[0].url).toBe('udp://tracker.example/announce') const createObjectRes = await app.request('/api/objects', { method: 'POST', diff --git a/server/routes/download-tasks.ts b/server/routes/download-tasks.ts index 628b51bc..1d1edcf2 100644 --- a/server/routes/download-tasks.ts +++ b/server/routes/download-tasks.ts @@ -1,5 +1,10 @@ import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi' -import { createDownloadTaskSchema, listDownloadTasksQuerySchema, updateDownloadTaskSchema } from '@shared/schemas' +import { + createDownloadTaskSchema, + downloadTaskDetailSchema, + listDownloadTasksQuerySchema, + updateDownloadTaskSchema, +} from '@shared/schemas' import type { Context } from 'hono' import { requireAuth, requireTeamRole } from '../middleware/auth' import type { Env } from '../middleware/platform' @@ -38,7 +43,9 @@ const downloadTaskSchema = z.object({ totalBytes: nullableInt64Schema(), downloadBps: int64Schema(), uploadBps: int64Schema(), + errorMessage: z.string().nullable().optional(), resultObjectId: z.string().nullable().optional(), + detail: downloadTaskDetailSchema.nullable().optional(), uploadToken: z.string().optional(), assignedDownloaderId: z.string().nullable().optional(), }) diff --git a/server/services/downloads.ts b/server/services/downloads.ts index 5f9f0dcb..b8be126d 100644 --- a/server/services/downloads.ts +++ b/server/services/downloads.ts @@ -288,7 +288,8 @@ export async function updateDownloadTask( input.downloadBps === undefined && input.uploadBps === undefined && input.errorMessage === undefined && - input.resultObjectId === undefined + input.resultObjectId === undefined && + input.detail === undefined if (!onlyCancel) throw new DownloadError('forbidden') } @@ -350,6 +351,7 @@ export async function updateDownloadTask( uploadBps: input.uploadBps ?? task.uploadBps, errorMessage: input.errorMessage === undefined ? task.errorMessage : input.errorMessage, resultObjectId: input.resultObjectId === undefined ? task.resultObjectId : input.resultObjectId, + detail: input.detail === undefined ? task.detail : JSON.stringify(input.detail), startedAt: task.startedAt ?? (status === 'running' ? now : null), finishedAt: ['completed', 'failed', 'canceled'].includes(status) ? now : task.finishedAt, updatedAt: now, @@ -511,6 +513,7 @@ function toDownloadTask(row: DownloadTaskRow): DownloadTask { uploadBps: row.uploadBps, errorMessage: row.errorMessage, resultObjectId: row.resultObjectId, + detail: parseTaskDetail(row.detail), createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString(), assignedAt: row.assignedAt?.toISOString() ?? null, @@ -519,6 +522,15 @@ function toDownloadTask(row: DownloadTaskRow): DownloadTask { } } +function parseTaskDetail(value: string | null): DownloadTask['detail'] { + if (!value) return null + try { + return JSON.parse(value) as DownloadTask['detail'] + } catch { + return null + } +} + function parseCapabilities(value: string): string[] { try { const parsed = JSON.parse(value) diff --git a/server/test/setup.ts b/server/test/setup.ts index 1cd67e76..b8c457ec 100644 --- a/server/test/setup.ts +++ b/server/test/setup.ts @@ -437,6 +437,7 @@ const APP_SCHEMA_SQL = ` upload_bps INTEGER NOT NULL DEFAULT 0, error_message TEXT, result_object_id TEXT, + detail TEXT, upload_token_hash TEXT, upload_token_jti TEXT, upload_token_expires_at INTEGER, diff --git a/shared/schemas/downloads.ts b/shared/schemas/downloads.ts index bf1428b6..2313a458 100644 --- a/shared/schemas/downloads.ts +++ b/shared/schemas/downloads.ts @@ -14,6 +14,47 @@ export const downloadTaskStatusSchema = z.enum([ ]) export const downloadSourceTypeSchema = z.enum(['http', 'magnet', 'torrent_url']) +const downloadTaskTrackerSchema = z.object({ + url: z.string().max(1024), + status: z.string().max(80).optional(), + peers: z.number().int().min(0).optional(), + seeds: z.number().int().min(0).optional(), + leechers: z.number().int().min(0).optional(), + message: z.string().max(500).optional(), +}) + +const downloadTaskPeerSchema = z.object({ + address: z.string().max(160), + client: z.string().max(160).optional(), + progress: z.number().min(0).max(1).optional(), + downloadBps: z.number().int().min(0).optional(), + uploadBps: z.number().int().min(0).optional(), +}) + +const downloadTaskFileSchema = z.object({ + path: z.string().max(1024), + size: z.number().int().min(0), + completedBytes: z.number().int().min(0).optional(), + selected: z.boolean().optional(), +}) + +export const downloadTaskDetailSchema = z.object({ + engine: downloaderEngineSchema.optional(), + phase: z.string().max(80).optional(), + message: z.string().max(500).optional(), + etaSeconds: z.number().int().min(0).nullable().optional(), + connections: z.number().int().min(0).optional(), + infoHash: z.string().max(120).optional(), + torrentName: z.string().max(255).optional(), + seeders: z.number().int().min(0).optional(), + leechers: z.number().int().min(0).optional(), + peers: z.number().int().min(0).optional(), + uploadedBytes: z.number().int().min(0).optional(), + trackers: z.array(downloadTaskTrackerSchema).max(20).optional(), + peerSamples: z.array(downloadTaskPeerSchema).max(20).optional(), + files: z.array(downloadTaskFileSchema).max(50).optional(), +}) + export const downloaderHeartbeatSchema = z.object({ version: z.string().min(1).max(80), hostname: z.string().min(1).max(160), @@ -60,6 +101,7 @@ export const updateDownloadTaskSchema = z.object({ uploadBps: z.number().int().min(0).optional(), errorMessage: z.string().max(1000).nullable().optional(), resultObjectId: z.string().min(1).nullable().optional(), + detail: downloadTaskDetailSchema.nullable().optional(), }) export const listDownloadTasksQuerySchema = z.object({ @@ -107,6 +149,7 @@ export type CreateDownloaderInput = z.infer export type CreateDownloadTaskInput = z.infer export type UpdateDownloadTaskInput = z.infer export type ListDownloadTasksQuery = z.infer +export type DownloadTaskDetail = z.infer export type CreateObjectUploadSessionInput = z.infer export type PresignObjectUploadPartsInput = z.infer export type PatchObjectUploadSessionInput = z.infer diff --git a/shared/schemas/index.ts b/shared/schemas/index.ts index 8fa1a889..cb481c88 100644 --- a/shared/schemas/index.ts +++ b/shared/schemas/index.ts @@ -76,6 +76,7 @@ export type { CreateDownloadTaskInput, CreateObjectUploadSessionInput, DownloaderHeartbeatInput, + DownloadTaskDetail, ListDownloadTasksQuery, PatchObjectUploadSessionInput, PresignObjectUploadPartsInput, @@ -90,6 +91,7 @@ export { downloaderHeartbeatSchema, downloaderStatusSchema, downloadSourceTypeSchema, + downloadTaskDetailSchema, downloadTaskStatusSchema, listDownloadTasksQuerySchema, patchObjectUploadSessionSchema, diff --git a/shared/types/index.ts b/shared/types/index.ts index 2f9e7815..649f019c 100644 --- a/shared/types/index.ts +++ b/shared/types/index.ts @@ -261,6 +261,7 @@ export interface DownloadTask { uploadBps: number errorMessage: string | null resultObjectId: string | null + detail: DownloadTaskDetail | null uploadToken?: string createdAt: string updatedAt: string @@ -269,6 +270,47 @@ export interface DownloadTask { finishedAt: string | null } +export interface DownloadTaskTracker { + url: string + status?: string + peers?: number + seeds?: number + leechers?: number + message?: string +} + +export interface DownloadTaskPeer { + address: string + client?: string + progress?: number + downloadBps?: number + uploadBps?: number +} + +export interface DownloadTaskFile { + path: string + size: number + completedBytes?: number + selected?: boolean +} + +export interface DownloadTaskDetail { + engine?: Downloader['engine'] + phase?: string + message?: string + etaSeconds?: number | null + connections?: number + infoHash?: string + torrentName?: string + seeders?: number + leechers?: number + peers?: number + uploadedBytes?: number + trackers?: DownloadTaskTracker[] + peerSamples?: DownloadTaskPeer[] + files?: DownloadTaskFile[] +} + export interface ObjectUploadSession { id: string objectId: string diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 90506f24..4acd548a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -146,6 +146,26 @@ "downloads.status.completed": "Completed", "downloads.status.failed": "Failed", "downloads.status.canceled": "Canceled", + "downloads.detail.downloadSpeed": "Download speed", + "downloads.detail.uploadSpeed": "Upload speed", + "downloads.detail.connections": "Connections", + "downloads.detail.eta": "ETA", + "downloads.detail.progress": "Progress", + "downloads.detail.target": "Target folder", + "downloads.detail.engine": "Engine", + "downloads.detail.billing": "Billing", + "downloads.detail.bt": "BT details", + "downloads.detail.infoHash": "Info hash", + "downloads.detail.seeders": "Seeders", + "downloads.detail.leechers": "Leechers", + "downloads.detail.uploaded": "Uploaded", + "downloads.detail.trackers": "Trackers", + "downloads.detail.noTrackers": "No tracker data yet", + "downloads.detail.peers": "Peers", + "downloads.detail.noPeers": "No peer data yet", + "downloads.detail.files": "Files", + "downloads.detail.noFiles": "No file data yet", + "downloads.detail.skipped": "Skipped", "uploadPanel.toggle": "Upload progress", "uploadPanel.title": "Uploads", "uploadPanel.empty": "No uploads yet", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index b0e3e55d..1fca06a2 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -146,6 +146,26 @@ "downloads.status.completed": "已完成", "downloads.status.failed": "失败", "downloads.status.canceled": "已取消", + "downloads.detail.downloadSpeed": "下载速度", + "downloads.detail.uploadSpeed": "上传速度", + "downloads.detail.connections": "连接数", + "downloads.detail.eta": "剩余时间", + "downloads.detail.progress": "完成度", + "downloads.detail.target": "目标目录", + "downloads.detail.engine": "下载引擎", + "downloads.detail.billing": "计费", + "downloads.detail.bt": "BT 信息", + "downloads.detail.infoHash": "Info hash", + "downloads.detail.seeders": "做种", + "downloads.detail.leechers": "下载者", + "downloads.detail.uploaded": "已上传", + "downloads.detail.trackers": "Trackers", + "downloads.detail.noTrackers": "暂无 tracker 信息", + "downloads.detail.peers": "Peers", + "downloads.detail.noPeers": "暂无 peer 信息", + "downloads.detail.files": "文件", + "downloads.detail.noFiles": "暂无文件信息", + "downloads.detail.skipped": "跳过", "uploadPanel.toggle": "上传进度", "uploadPanel.title": "上传", "uploadPanel.empty": "暂无上传任务", diff --git a/src/routes/_authenticated/downloads/index.tsx b/src/routes/_authenticated/downloads/index.tsx index 0d30ad00..79359560 100644 --- a/src/routes/_authenticated/downloads/index.tsx +++ b/src/routes/_authenticated/downloads/index.tsx @@ -3,20 +3,27 @@ import type { DownloadTask, DownloadTaskStatus, StorageObject } from '@shared/ty import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { createFileRoute } from '@tanstack/react-router' import { + AlertCircle, Check, + ChevronDown, ChevronRight, + Clock, Download, FileDown, Folder, FolderInput, + Gauge, Home, LinkIcon, Magnet, PauseCircle, Plus, + RadioTower, RotateCw, + Upload, + Users, } from 'lucide-react' -import { type FormEvent, useState } from 'react' +import { type FormEvent, Fragment, type ReactNode, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { useFilesQuery } from '@/components/files/hooks/use-files-query' @@ -48,6 +55,7 @@ function DownloadsPage() { const [targetFolder, setTargetFolder] = useState('') const [name, setName] = useState('') const [createOpen, setCreateOpen] = useState(false) + const [expandedTaskId, setExpandedTaskId] = useState(null) const tasksQuery = useQuery({ queryKey: QUERY_KEY, @@ -198,7 +206,13 @@ function DownloadsPage() { )} {tasks.map((task) => ( - pauseMutation.mutate(id)} /> + setExpandedTaskId((current) => (current === task.id ? null : task.id))} + onCancel={(id) => pauseMutation.mutate(id)} + /> ))} @@ -319,51 +333,238 @@ function FolderPicker({ value, onChange }: { value: string; onChange: (path: str ) } -function TaskRow({ task, onCancel }: { task: DownloadTask; onCancel: (id: string) => void }) { +function TaskRow({ + task, + expanded, + onToggle, + onCancel, +}: { + task: DownloadTask + expanded: boolean + onToggle: () => void + onCancel: (id: string) => void +}) { const { t } = useTranslation() const total = task.totalBytes ?? task.downloadedBytes const progress = total > 0 ? Math.min(100, Math.round((task.downloadedBytes / total) * 100)) : 0 const active = ACTIVE_STATUSES.has(task.status) return ( - - -
- -
-
{task.name || task.sourceUri}
-
{task.sourceUri}
+ + + +
+ + +
+
{task.detail?.torrentName || task.name || task.sourceUri}
+
{task.sourceUri}
+
+
+
+ + + {task.detail?.phase &&
{task.detail.phase}
} + {task.status === 'billing_paused' && ( +
{t('downloads.billingPaused')}
+ )} +
+ +
+ + {progress}% +
+
+ {formatBytes(task.downloadedBytes)} /{' '} + {task.totalBytes ? formatBytes(task.totalBytes) : t('downloads.unknown')} +
+
+ +
{formatBytes(task.downloadBps)}/s
+ {task.uploadBps > 0 && ( +
{formatBytes(task.uploadBps)}/s up
+ )} +
+ + {active ? ( + + ) : ( + + )} + +
+ {expanded && ( + + + + + + )} +
+ ) +} + +function TaskDetails({ task, progress }: { task: DownloadTask; progress: number }) { + const { t } = useTranslation() + const detail = task.detail + const hasBtDetail = Boolean( + detail?.infoHash || detail?.trackers?.length || detail?.peerSamples?.length || detail?.files?.length, + ) + + return ( +
+
+ } + label={t('downloads.detail.downloadSpeed')} + value={`${formatBytes(task.downloadBps)}/s`} + /> + } + label={t('downloads.detail.uploadSpeed')} + value={`${formatBytes(task.uploadBps)}/s`} + /> + } + label={t('downloads.detail.connections')} + value={formatNumber(detail?.connections)} + /> + } + label={t('downloads.detail.eta')} + value={formatDuration(detail?.etaSeconds)} + /> +
+ +
+ + + + +
+ + {task.errorMessage && ( +
+ +
{task.errorMessage}
+
+ )} + + {hasBtDetail && ( +
+
+ } title={t('downloads.detail.bt')} /> +
+ + + + +
+
+ +
+ } title={t('downloads.detail.trackers')} /> + ({ + key: tracker.url, + primary: tracker.url, + secondary: [tracker.status, tracker.message].filter(Boolean).join(' · '), + meta: `${formatNumber(tracker.seeds)} / ${formatNumber(tracker.peers)}`, + }))} + /> +
+ +
+ } title={t('downloads.detail.peers')} /> + ({ + key: peer.address, + primary: peer.address, + secondary: peer.client, + meta: `${formatBytes(peer.downloadBps ?? 0)}/s`, + }))} + /> +
+ +
+ } title={t('downloads.detail.files')} /> + ({ + key: file.path, + primary: file.path, + secondary: `${formatBytes(file.completedBytes ?? 0)} / ${formatBytes(file.size)}`, + meta: file.selected === false ? t('downloads.detail.skipped') : '', + }))} + />
- - - - {task.status === 'billing_paused' && ( -
{t('downloads.billingPaused')}
- )} -
- -
- - {progress}% + )} +
+ ) +} + +function Metric({ icon, label, value }: { icon: ReactNode; label: string; value: string }) { + return ( +
+
+ {icon} + {label} +
+
{value}
+
+ ) +} + +function DetailItem({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ) +} + +function SectionTitle({ icon, title }: { icon: ReactNode; title: string }) { + return ( +
+ {icon} + {title} +
+ ) +} + +function CompactList({ + items, + empty, +}: { + items: Array<{ key: string; primary: string; secondary?: string; meta?: string }> + empty: string +}) { + if (items.length === 0) + return
{empty}
+ return ( +
+ {items.map((item) => ( +
+
+
{item.primary}
+ {item.secondary &&
{item.secondary}
} +
+ {item.meta &&
{item.meta}
}
-
- {formatBytes(task.downloadedBytes)} /{' '} - {task.totalBytes ? formatBytes(task.totalBytes) : t('downloads.unknown')} -
- - {formatBytes(task.downloadBps)}/s - - {active ? ( - - ) : ( - - )} - - + ))} +
) } @@ -385,3 +586,17 @@ function formatBytes(bytes: number) { } return `${value >= 10 || unit === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unit]}` } + +function formatNumber(value: number | null | undefined) { + if (!Number.isFinite(value)) return '-' + return new Intl.NumberFormat().format(value as number) +} + +function formatDuration(seconds: number | null | undefined) { + if (!Number.isFinite(seconds) || seconds === null || seconds === undefined) return '-' + if (seconds < 60) return `${seconds}s` + const minutes = Math.floor(seconds / 60) + if (minutes < 60) return `${minutes}m ${seconds % 60}s` + const hours = Math.floor(minutes / 60) + return `${hours}h ${minutes % 60}m` +}