Init V4 community edition (#2265)
* Init V4 community edition * Init V4 community edition
This commit is contained in:
436
service/explorer/response.go
Normal file
436
service/explorer/response.go
Normal file
@@ -0,0 +1,436 @@
|
||||
package explorer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/cloudreve/Cloudreve/v4/application/dependency"
|
||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/auth"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/queue"
|
||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||
"github.com/cloudreve/Cloudreve/v4/service/user"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type DirectLinkResponse struct {
|
||||
Link string `json:"link"`
|
||||
FileUrl string `json:"file_url"`
|
||||
}
|
||||
|
||||
func BuildDirectLinkResponse(links []manager.DirectLink) []DirectLinkResponse {
|
||||
if len(links) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var res []DirectLinkResponse
|
||||
for _, link := range links {
|
||||
res = append(res, DirectLinkResponse{
|
||||
Link: link.Url,
|
||||
FileUrl: link.File.Uri(false).String(),
|
||||
})
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
const PathMyRedacted = "redacted"
|
||||
|
||||
type TaskResponse struct {
|
||||
CreatedAt time.Time `json:"created_at,"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Type string `json:"type"`
|
||||
Node *user.Node `json:"node,omitempty"`
|
||||
Summary *queue.Summary `json:"summary,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
ErrorHistory []string `json:"error_history,omitempty"`
|
||||
Duration int64 `json:"duration,omitempty"`
|
||||
ResumeTime int64 `json:"resume_time,omitempty"`
|
||||
RetryCount int `json:"retry_count,omitempty"`
|
||||
}
|
||||
|
||||
type TaskListResponse struct {
|
||||
Tasks []TaskResponse `json:"tasks"`
|
||||
Pagination *inventory.PaginationResults `json:"pagination"`
|
||||
}
|
||||
|
||||
func BuildTaskListResponse(tasks []queue.Task, res *inventory.ListTaskResult, nodeMap map[int]*ent.Node, hasher hashid.Encoder) *TaskListResponse {
|
||||
return &TaskListResponse{
|
||||
Pagination: res.PaginationResults,
|
||||
Tasks: lo.Map(tasks, func(t queue.Task, index int) TaskResponse {
|
||||
var (
|
||||
node *ent.Node
|
||||
s = t.Summarize(hasher)
|
||||
)
|
||||
|
||||
if s.NodeID > 0 {
|
||||
node = nodeMap[s.NodeID]
|
||||
}
|
||||
return *BuildTaskResponse(t, node, hasher)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func BuildTaskResponse(task queue.Task, node *ent.Node, hasher hashid.Encoder) *TaskResponse {
|
||||
model := task.Model()
|
||||
t := &TaskResponse{
|
||||
Status: string(task.Status()),
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
ID: hashid.EncodeTaskID(hasher, task.ID()),
|
||||
Type: task.Type(),
|
||||
Summary: task.Summarize(hasher),
|
||||
Error: auth.RedactSensitiveValues(model.PublicState.Error),
|
||||
ErrorHistory: lo.Map(model.PublicState.ErrorHistory, func(s string, index int) string {
|
||||
return auth.RedactSensitiveValues(s)
|
||||
}),
|
||||
Duration: model.PublicState.ExecutedDuration.Milliseconds(),
|
||||
ResumeTime: model.PublicState.ResumeTime,
|
||||
RetryCount: model.PublicState.RetryCount,
|
||||
}
|
||||
|
||||
if node != nil {
|
||||
t.Node = user.BuildNode(node, hasher)
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
type UploadSessionResponse struct {
|
||||
SessionID string `json:"session_id"`
|
||||
UploadID string `json:"upload_id"`
|
||||
ChunkSize int64 `json:"chunk_size"` // 分块大小,0 为部分快
|
||||
Expires int64 `json:"expires"` // 上传凭证过期时间, Unix 时间戳
|
||||
UploadURLs []string `json:"upload_urls,omitempty"`
|
||||
Credential string `json:"credential,omitempty"`
|
||||
AccessKey string `json:"ak,omitempty"`
|
||||
KeyTime string `json:"keyTime,omitempty"` // COS用有效期
|
||||
CompleteURL string `json:"completeURL,omitempty"`
|
||||
StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"`
|
||||
Uri string `json:"uri"`
|
||||
CallbackSecret string `json:"callback_secret"`
|
||||
MimeType string `json:"mime_type,omitempty"`
|
||||
UploadPolicy string `json:"upload_policy,omitempty"`
|
||||
}
|
||||
|
||||
func BuildUploadSessionResponse(session *fs.UploadCredential, hasher hashid.Encoder) *UploadSessionResponse {
|
||||
return &UploadSessionResponse{
|
||||
SessionID: session.SessionID,
|
||||
ChunkSize: session.ChunkSize,
|
||||
Expires: session.Expires,
|
||||
UploadURLs: session.UploadURLs,
|
||||
Credential: session.Credential,
|
||||
CompleteURL: session.CompleteURL,
|
||||
Uri: session.Uri,
|
||||
UploadID: session.UploadID,
|
||||
StoragePolicy: BuildStoragePolicy(session.StoragePolicy, hasher),
|
||||
CallbackSecret: session.CallbackSecret,
|
||||
MimeType: session.MimeType,
|
||||
UploadPolicy: session.UploadPolicy,
|
||||
}
|
||||
}
|
||||
|
||||
// WopiFileInfo Response for `CheckFileInfo`
|
||||
type WopiFileInfo struct {
|
||||
// Required
|
||||
BaseFileName string
|
||||
Version string
|
||||
Size int64
|
||||
|
||||
// Breadcrumb
|
||||
BreadcrumbBrandName string
|
||||
BreadcrumbBrandUrl string
|
||||
BreadcrumbFolderName string
|
||||
BreadcrumbFolderUrl string
|
||||
|
||||
// Post Message
|
||||
FileSharingPostMessage bool
|
||||
FileVersionPostMessage bool
|
||||
ClosePostMessage bool
|
||||
PostMessageOrigin string
|
||||
|
||||
// Other miscellaneous properties
|
||||
FileNameMaxLength int
|
||||
LastModifiedTime string
|
||||
|
||||
// User metadata
|
||||
IsAnonymousUser bool
|
||||
UserFriendlyName string
|
||||
UserId string
|
||||
OwnerId string
|
||||
|
||||
// Permission
|
||||
ReadOnly bool
|
||||
UserCanRename bool
|
||||
UserCanReview bool
|
||||
UserCanWrite bool
|
||||
|
||||
SupportsRename bool
|
||||
SupportsReviewing bool
|
||||
SupportsUpdate bool
|
||||
SupportsLocks bool
|
||||
|
||||
EnableShare bool
|
||||
}
|
||||
|
||||
type ViewerSessionResponse struct {
|
||||
Session *manager.ViewerSession `json:"session"`
|
||||
WopiSrc string `json:"wopi_src,omitempty"`
|
||||
}
|
||||
|
||||
type ListResponse struct {
|
||||
Files []FileResponse `json:"files"`
|
||||
Parent FileResponse `json:"parent,omitempty"`
|
||||
Pagination *inventory.PaginationResults `json:"pagination"`
|
||||
Props *fs.NavigatorProps `json:"props"`
|
||||
// ContextHint is used to speed up following operations under this listed directory.
|
||||
// It persists some intermedia state so that the following request don't need to query database again.
|
||||
// All the operations under this directory that supports context hint should carry this value in header
|
||||
// as X-Cr-Context-Hint.
|
||||
ContextHint *uuid.UUID `json:"context_hint"`
|
||||
RecursionLimitReached bool `json:"recursion_limit_reached,omitempty"`
|
||||
MixedType bool `json:"mixed_type"`
|
||||
SingleFileView bool `json:"single_file_view,omitempty"`
|
||||
StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"`
|
||||
}
|
||||
|
||||
type FileResponse struct {
|
||||
Type int `json:"type"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Size int64 `json:"size"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Shared bool `json:"shared,omitempty"`
|
||||
Capability *boolset.BooleanSet `json:"capability,omitempty"`
|
||||
Owned bool `json:"owned,omitempty"`
|
||||
PrimaryEntity string `json:"primary_entity,omitempty"`
|
||||
|
||||
FolderSummary *fs.FolderSummary `json:"folder_summary,omitempty"`
|
||||
ExtendedInfo *ExtendedInfo `json:"extended_info,omitempty"`
|
||||
}
|
||||
|
||||
type ExtendedInfo struct {
|
||||
StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"`
|
||||
StorageUsed int64 `json:"storage_used"`
|
||||
Shares []Share `json:"shares,omitempty"`
|
||||
Entities []Entity `json:"entities,omitempty"`
|
||||
}
|
||||
|
||||
type StoragePolicy struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
AllowedSuffix []string `json:"allowed_suffix,omitempty"`
|
||||
Type types.PolicyType `json:"type"`
|
||||
MaxSize int64 `json:"max_size"`
|
||||
Relay bool `json:"relay,omitempty"`
|
||||
}
|
||||
|
||||
type Entity struct {
|
||||
ID string `json:"id"`
|
||||
Size int64 `json:"size"`
|
||||
Type types.EntityType `json:"type"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
StoragePolicy *StoragePolicy `json:"storage_policy,omitempty"`
|
||||
CreatedBy *user.User `json:"created_by,omitempty"`
|
||||
}
|
||||
|
||||
type Share struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
RemainDownloads *int `json:"remain_downloads,omitempty"`
|
||||
Visited int `json:"visited"`
|
||||
Downloaded int `json:"downloaded,omitempty"`
|
||||
Expires *time.Time `json:"expires,omitempty"`
|
||||
Unlocked bool `json:"unlocked"`
|
||||
SourceType *types.FileType `json:"source_type,omitempty"`
|
||||
Owner user.User `json:"owner"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
Expired bool `json:"expired"`
|
||||
Url string `json:"url"`
|
||||
|
||||
// Only viewable by owner
|
||||
IsPrivate bool `json:"is_private,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
|
||||
// Only viewable if explicitly unlocked by owner
|
||||
SourceUri string `json:"source_uri,omitempty"`
|
||||
}
|
||||
|
||||
func BuildShare(s *ent.Share, base *url.URL, hasher hashid.Encoder, requester *ent.User, owner *ent.User,
|
||||
name string, t types.FileType, unlocked bool) *Share {
|
||||
redactLevel := user.RedactLevelAnonymous
|
||||
if !inventory.IsAnonymousUser(requester) {
|
||||
redactLevel = user.RedactLevelUser
|
||||
}
|
||||
res := Share{
|
||||
Name: name,
|
||||
ID: hashid.EncodeShareID(hasher, s.ID),
|
||||
Unlocked: unlocked,
|
||||
Owner: user.BuildUserRedacted(owner, redactLevel, hasher),
|
||||
Expired: inventory.IsShareExpired(s) != nil,
|
||||
Url: BuildShareLink(s, hasher, base),
|
||||
CreatedAt: s.CreatedAt,
|
||||
Visited: s.Views,
|
||||
SourceType: util.ToPtr(t),
|
||||
}
|
||||
|
||||
if unlocked {
|
||||
res.RemainDownloads = s.RemainDownloads
|
||||
res.Downloaded = s.Downloads
|
||||
res.Expires = s.Expires
|
||||
res.Password = s.Password
|
||||
}
|
||||
|
||||
if requester.ID == owner.ID {
|
||||
res.IsPrivate = s.Password != ""
|
||||
}
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
func BuildListResponse(ctx context.Context, u *ent.User, parent fs.File, res *fs.ListFileResult, hasher hashid.Encoder) *ListResponse {
|
||||
r := &ListResponse{
|
||||
Files: lo.Map(res.Files, func(f fs.File, index int) FileResponse {
|
||||
return *BuildFileResponse(ctx, u, f, hasher, res.Props.Capability)
|
||||
}),
|
||||
Pagination: res.Pagination,
|
||||
Props: res.Props,
|
||||
ContextHint: res.ContextHint,
|
||||
RecursionLimitReached: res.RecursionLimitReached,
|
||||
MixedType: res.MixedType,
|
||||
SingleFileView: res.SingleFileView,
|
||||
StoragePolicy: BuildStoragePolicy(res.StoragePolicy, hasher),
|
||||
}
|
||||
|
||||
if !res.Parent.IsNil() {
|
||||
r.Parent = *BuildFileResponse(ctx, u, res.Parent, hasher, res.Props.Capability)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func BuildFileResponse(ctx context.Context, u *ent.User, f fs.File, hasher hashid.Encoder, cap *boolset.BooleanSet) *FileResponse {
|
||||
var owner *ent.User
|
||||
if f != nil {
|
||||
owner = f.Owner()
|
||||
}
|
||||
|
||||
if cap == nil {
|
||||
cap = f.Capabilities()
|
||||
}
|
||||
|
||||
res := &FileResponse{
|
||||
Type: int(f.Type()),
|
||||
ID: hashid.EncodeFileID(hasher, f.ID()),
|
||||
Name: f.DisplayName(),
|
||||
CreatedAt: f.CreatedAt(),
|
||||
UpdatedAt: f.UpdatedAt(),
|
||||
Size: f.Size(),
|
||||
Metadata: f.Metadata(),
|
||||
Path: f.Uri(false).String(),
|
||||
Shared: f.Shared(),
|
||||
Capability: cap,
|
||||
Owned: owner == nil || owner.ID == u.ID,
|
||||
FolderSummary: f.FolderSummary(),
|
||||
ExtendedInfo: BuildExtendedInfo(ctx, u, f, hasher),
|
||||
PrimaryEntity: hashid.EncodeEntityID(hasher, f.PrimaryEntityID()),
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func BuildExtendedInfo(ctx context.Context, u *ent.User, f fs.File, hasher hashid.Encoder) *ExtendedInfo {
|
||||
extendedInfo := f.ExtendedInfo()
|
||||
if extendedInfo == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ext := &ExtendedInfo{
|
||||
StoragePolicy: BuildStoragePolicy(extendedInfo.StoragePolicy, hasher),
|
||||
StorageUsed: extendedInfo.StorageUsed,
|
||||
Entities: lo.Map(f.Entities(), func(e fs.Entity, index int) Entity {
|
||||
return BuildEntity(extendedInfo, e, hasher)
|
||||
}),
|
||||
}
|
||||
|
||||
dep := dependency.FromContext(ctx)
|
||||
base := dep.SettingProvider().SiteURL(ctx)
|
||||
if u.ID == f.OwnerID() {
|
||||
// Only owner can see the shares settings.
|
||||
ext.Shares = lo.Map(extendedInfo.Shares, func(s *ent.Share, index int) Share {
|
||||
return *BuildShare(s, base, hasher, u, u, f.DisplayName(), f.Type(), true)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return ext
|
||||
}
|
||||
|
||||
func BuildEntity(extendedInfo *fs.FileExtendedInfo, e fs.Entity, hasher hashid.Encoder) Entity {
|
||||
var u *user.User
|
||||
createdBy := e.CreatedBy()
|
||||
if createdBy != nil {
|
||||
userRedacted := user.BuildUserRedacted(e.CreatedBy(), user.RedactLevelAnonymous, hasher)
|
||||
u = &userRedacted
|
||||
}
|
||||
return Entity{
|
||||
ID: hashid.EncodeEntityID(hasher, e.ID()),
|
||||
Type: e.Type(),
|
||||
CreatedAt: e.CreatedAt(),
|
||||
StoragePolicy: BuildStoragePolicy(extendedInfo.EntityStoragePolicies[e.PolicyID()], hasher),
|
||||
Size: e.Size(),
|
||||
CreatedBy: u,
|
||||
}
|
||||
}
|
||||
|
||||
func BuildShareLink(s *ent.Share, hasher hashid.Encoder, base *url.URL) string {
|
||||
shareId := hashid.EncodeShareID(hasher, s.ID)
|
||||
return routes.MasterShareUrl(base, shareId, s.Password).String()
|
||||
}
|
||||
|
||||
func BuildStoragePolicy(sp *ent.StoragePolicy, hasher hashid.Encoder) *StoragePolicy {
|
||||
if sp == nil {
|
||||
return nil
|
||||
}
|
||||
return &StoragePolicy{
|
||||
ID: hashid.EncodePolicyID(hasher, sp.ID),
|
||||
Name: sp.Name,
|
||||
Type: types.PolicyType(sp.Type),
|
||||
MaxSize: sp.MaxSize,
|
||||
AllowedSuffix: sp.Settings.FileType,
|
||||
Relay: sp.Settings.Relay,
|
||||
}
|
||||
}
|
||||
|
||||
func WriteEventSourceHeader(c *gin.Context) {
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("X-Accel-Buffering", "no")
|
||||
}
|
||||
|
||||
// WriteEventSource writes a Server-Sent Event to the client.
|
||||
func WriteEventSource(c *gin.Context, event string, data any) {
|
||||
c.Writer.Write([]byte(fmt.Sprintf("event: %s\n", event)))
|
||||
c.Writer.Write([]byte("data:"))
|
||||
json.NewEncoder(c.Writer).Encode(data)
|
||||
c.Writer.Write([]byte("\n"))
|
||||
c.Writer.Flush()
|
||||
}
|
||||
|
||||
var ErrSSETakeOver = errors.New("SSE take over")
|
||||
Reference in New Issue
Block a user