diff options
Diffstat (limited to 'backend/studip')
| -rw-r--r-- | backend/studip/fs.go | 290 | ||||
| -rw-r--r-- | backend/studip/requests.go | 283 | ||||
| -rw-r--r-- | backend/studip/responses.go | 47 |
3 files changed, 607 insertions, 13 deletions
diff --git a/backend/studip/fs.go b/backend/studip/fs.go index 5aaf392..df18171 100644 --- a/backend/studip/fs.go +++ b/backend/studip/fs.go @@ -452,6 +452,25 @@ func lockMutationCourses(fss ...*Fs) func() { } } +func (f *Fs) objectFromFileRefData(remote string, data *StudIPFileRefData) *Object { + if data == nil { + return nil + } + + return &Object{ + fs: f, + remote: cleanPath(remote), + id: data.ID, + size: data.Attributes.Filesize, + isReadable: data.Attributes.IsReadable, + isEditable: data.Attributes.IsEditable, + isWritable: data.Attributes.IsWritable, + IsDownloadable: data.Attributes.IsDownloadable, + contentType: data.Attributes.MimeType, + modTime: data.Attributes.Chdate, + } +} + func updateSubtreePaths(node *Node) { if node == nil { return @@ -469,6 +488,65 @@ func updateSubtreePaths(node *Node) { } } +func applyFileRefData(node *Node, data *StudIPFileRefData) { + if node == nil || data == nil { + return + } + + node.ID = data.ID + node.IsReadable = data.Attributes.IsReadable + node.IsWritable = data.Attributes.IsWritable + node.IsEditable = data.Attributes.IsEditable + node.IsDownloadable = data.Attributes.IsDownloadable + node.ContentType = data.Attributes.MimeType + node.Size = data.Attributes.Filesize + node.ChDate = data.Attributes.Chdate +} + +func (f *Fs) moveNodeInTree(sourceAbs, destAbs string, fileData *StudIPFileRefData) bool { + f.mu.Lock() + defer f.mu.Unlock() + + if f.ft.root == nil { + return false + } + + sourceNode := f.ft.root.GetNodeAtPath(sourceAbs) + if sourceNode == nil { + sourceNode = f.ft.root.GetNodeAtPath(sourceAbs) + } + if sourceNode == nil { + return false + } + + destParent := f.ft.root.GetNodeAtPath(dirPath(destAbs)) + if destParent == nil { + destParent = f.ft.root.GetNodeAtPath(dirPath(destAbs)) + } + if destParent == nil || !destParent.IsDir { + return false + } + + if sourceNode.Parent != nil { + index := slices.Index(sourceNode.Parent.Children, sourceNode) + if index >= 0 { + sourceNode.Parent.Children = slices.Delete(sourceNode.Parent.Children, index, index+1) + } + } + + sourceNode.Name = basePath(destAbs) + sourceNode.Parent = destParent + sourceNode.Path = joinPath(destParent.Path, sourceNode.Name) + if !sourceNode.IsDir { + applyFileRefData(sourceNode, fileData) + } + destParent.Children = append(destParent.Children, sourceNode) + updateSubtreePaths(sourceNode) + f.updateRelativeRootFromTree() + f.bumpTreeGenerationAndMarkCurrent() + + return true +} func (f *Fs) GetCourseFileTree( ctx context.Context, @@ -1393,17 +1471,215 @@ func (f *Fs) Features() *fs.Features { return (&fs.Features{ CanHaveEmptyDirectories: true, CaseInsensitive: true, - //ReadMimeType: true, - // TODO: Implement these - Copy: nil, - Move: nil, - DirMove: nil, - // implement this - Purge: nil, + Copy: nil, + Purge: nil, }). Fill(context.Background(), f) } +func (f *Fs) Move( + ctx context.Context, + src fs.Object, + remote string, +) (fs.Object, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + remote = cleanPath(remote) + if remote == "" { + return nil, fs.ErrorCantMove + } + + srcObject, ok := src.(*Object) + if !ok || srcObject == nil || srcObject.fs == nil { + return nil, fs.ErrorCantMove + } + + srcFs := srcObject.fs + if !f.sameCourse(srcFs) { + return nil, fs.ErrorCantMove + } + + unlockCourses := lockMutationCourses(srcFs, f) + defer unlockCourses() + + if err := srcFs.ensureCurrentFileTree(ctx); err != nil { + return nil, err + } + if srcFs != f { + if err := f.ensureCurrentFileTree(ctx); err != nil { + return nil, err + } + } + + endMutations := beginMutations(srcFs, f) + defer endMutations() + + sourceAbs := joinPath(cleanPath(srcFs.relativeRootPath), srcObject.remote) + destAbs := joinPath(cleanPath(f.relativeRootPath), remote) + if sourceAbs == destAbs { + fileRef, err := f.studIPGetFileRef(ctx, srcObject.id) + if err != nil { + return nil, err + } + return f.objectFromFileRefData(remote, &fileRef.Data), nil + } + + var sourceNode *Node + { + srcFs.mu.RLock() + + if srcFs.ft.root != nil { + sourceNode = srcFs.ft.root.GetNodeAtPath(sourceAbs) + } + + srcFs.mu.RUnlock() + } + + if sourceNode == nil || sourceNode.IsDir { + return nil, fs.ErrorCantMove + } + + fileRef, err := srcFs.studIPGetFileRef(ctx, srcObject.id) + if err != nil { + return nil, err + } + + destName := basePath(destAbs) + if destName == "" { + return nil, fs.ErrorCantMove + } + + destName = f.opt.Enc.FromStandardName(destName) + + var moved *StudIPFileRefData + if dirPath(sourceAbs) == dirPath(destAbs) { + moved, err = f.studIPUpdateFileRef( + ctx, + srcObject.id, + destName, + fileRef.Data.Attributes.Description, + f.opt.License, + ) + if err != nil { + return nil, err + } + } else { + destParentNode, err := f.CreateParentDirectories(ctx, dirPath(destAbs)) + if err != nil { + return nil, err + } + + moved, err = f.studIPCreateFileRefByReference( + ctx, + destParentNode.ID, + fileRef.Data.Relationships.File.Data.ID, + destName, + fileRef.Data.Attributes.Description, + f.opt.License, + ) + if err != nil { + return nil, err + } + + if err := srcFs.studIPDeleteFile(ctx, srcObject.id); err != nil { + if cleanupErr := f.studIPDeleteFile(ctx, moved.ID); cleanupErr != nil { + fs.Debugf( + f, + "Move: cleanup after delete failure failed newID=%q err=%v", + moved.ID, + cleanupErr, + ) + } + return nil, err + } + } + + if !f.moveNodeInTree(sourceAbs, destAbs, moved) { + f.fileTreeGenerationCounter().Add(1) + } + + return f.objectFromFileRefData(remote, moved), nil +} + +func (f *Fs) DirMove( + ctx context.Context, + src fs.Fs, + srcRemote string, + dstRemote string, +) error { + if ctx.Err() != nil { + return ctx.Err() + } + + srcFs, ok := src.(*Fs) + if !ok || !f.sameCourse(srcFs) { + return fs.ErrorCantDirMove + } + + srcRemote = cleanPath(srcRemote) + dstRemote = cleanPath(dstRemote) + if srcRemote == dstRemote { + return fs.ErrorDirExists + } + + unlockCourses := lockMutationCourses(srcFs, f) + defer unlockCourses() + + if err := srcFs.ensureCurrentFileTree(ctx); err != nil { + return err + } + if srcFs != f { + if err := f.ensureCurrentFileTree(ctx); err != nil { + return err + } + } + + sourceAbs := joinPath(cleanPath(srcFs.relativeRootPath), srcRemote) + destAbs := joinPath(cleanPath(f.relativeRootPath), dstRemote) + + var sourceNode *Node + var existingNode *Node + if srcFs.ft.root != nil { + srcFs.mu.RLock() + sourceNode = srcFs.ft.root.GetNodeAtPath(sourceAbs) + existingNode = srcFs.ft.root.GetNodeAtPath(destAbs) + srcFs.mu.RUnlock() + } + + if existingNode != nil { + return fs.ErrorDirExists + } + + if sourceNode == nil || !sourceNode.IsDir || sourceNode.Parent == nil { + return fs.ErrorCantDirMove + } + + endMutations := beginMutations(srcFs, f) + defer endMutations() + + destParentNode, err := f.CreateParentDirectories(ctx, dirPath(destAbs)) + if err != nil { + return err + } + + if err := f.studIPUpdateFolder( + ctx, + sourceNode.ID, + basePath(destAbs), + destParentNode.ID, + ); err != nil { + return err + } + + if !f.moveNodeInTree(sourceAbs, destAbs, nil) { + f.fileTreeGenerationCounter().Add(1) + } + + return nil +} + func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { if ctx.Err() != nil { return nil, ctx.Err() diff --git a/backend/studip/requests.go b/backend/studip/requests.go index 5a637f8..592f938 100644 --- a/backend/studip/requests.go +++ b/backend/studip/requests.go @@ -338,6 +338,289 @@ func (f *Fs) studIPGetCourse(ctx context.Context) (*StudIPCourses, error) { return responseJSON, nil } +func (f *Fs) studIPGetFileRef( + ctx context.Context, + fileRefID string, +) (*StudIPFileRef, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + Assert( + f != nil, + fmt.Sprintf( + "f must be not nil; f=%q", + f, + ), + ) + + if fileRefID == "" { + return nil, errors.New("fileRefID is empty") + } + + URL := fmt.Sprintf("file-refs/%s", fileRefID) + responseJSON := new(StudIPFileRef) + res, err := f.client.CallJSON( + ctx, + &rest.Opts{Method: "GET", Path: URL}, + nil, + responseJSON, + ) + if err != nil { + return nil, err + } + defer res.Body.Close() + + return responseJSON, nil +} + +func (f *Fs) studIPUpdateFileRef( + ctx context.Context, + fileRefID string, + name string, + description string, + termsOfUseID string, +) (*StudIPFileRefData, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + Assert( + f != nil, + fmt.Sprintf( + "f must be not nil; f=%q", + f, + ), + ) + + if fileRefID == "" { + return nil, errors.New("fileRefID is empty") + } + + if name == "" { + return nil, errors.New("name is empty") + } + + if termsOfUseID == "" { + return nil, errors.New("termsOfUseID is empty") + } + + URL := fmt.Sprintf("file-refs/%s", fileRefID) + + payload := struct { + Data struct { + Type string `json:"type"` + Attributes struct { + Name string `json:"name"` + Description string `json:"description"` + } `json:"attributes"` + Relationships struct { + TermsOfUse struct { + Data struct { + Type string `json:"type"` + ID string `json:"id"` + } `json:"data"` + } `json:"terms-of-use"` + } `json:"relationships"` + } `json:"data"` + }{} + payload.Data.Type = "file-refs" + payload.Data.Attributes.Name = name + payload.Data.Attributes.Description = description + payload.Data.Relationships.TermsOfUse.Data.Type = "terms-of-use" + payload.Data.Relationships.TermsOfUse.Data.ID = termsOfUseID + + body, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + responseJSON := new(StudIPFileRef) + res, err := f.client.CallJSON( + ctx, + &rest.Opts{ + Method: "PATCH", + Path: URL, + ContentType: "application/vnd.api+json", + Body: bytes.NewReader(body), + }, + nil, + responseJSON, + ) + if err != nil { + return nil, err + } + defer res.Body.Close() + + return &responseJSON.Data, nil +} + +func (f *Fs) studIPCreateFileRefByReference( + ctx context.Context, + parentFolderID string, + fileID string, + name string, + description string, + termsOfUseID string, +) (*StudIPFileRefData, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + Assert( + f != nil, + fmt.Sprintf( + "f must be not nil; f=%q", + f, + ), + ) + + if parentFolderID == "" { + return nil, errors.New("parentFolderID is empty") + } + + if fileID == "" { + return nil, errors.New("fileID is empty") + } + + if name == "" { + return nil, errors.New("name is empty") + } + + if termsOfUseID == "" { + return nil, errors.New("termsOfUseID is empty") + } + + URL := fmt.Sprintf("folders/%s/file-refs", parentFolderID) + apiName := f.opt.Enc.FromStandardName(name) + + payload := struct { + Data struct { + Type string `json:"type"` + Attributes struct { + Name string `json:"name"` + Description string `json:"description"` + } `json:"attributes"` + Relationships struct { + File struct { + Data struct { + Type string `json:"type"` + ID string `json:"id"` + } `json:"data"` + } `json:"file"` + TermsOfUse struct { + Data struct { + Type string `json:"type"` + ID string `json:"id"` + } `json:"data"` + } `json:"terms-of-use"` + } `json:"relationships"` + } `json:"data"` + }{} + payload.Data.Type = "file-refs" + payload.Data.Attributes.Name = apiName + payload.Data.Attributes.Description = description + payload.Data.Relationships.File.Data.Type = "files" + payload.Data.Relationships.File.Data.ID = fileID + payload.Data.Relationships.TermsOfUse.Data.Type = "terms-of-use" + payload.Data.Relationships.TermsOfUse.Data.ID = termsOfUseID + + body, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + responseJSON := new(StudIPFileRef) + res, err := f.client.CallJSON( + ctx, + &rest.Opts{ + Method: "POST", + Path: URL, + ContentType: "application/vnd.api+json", + Body: bytes.NewReader(body), + }, + nil, + responseJSON, + ) + if err != nil { + return nil, err + } + defer res.Body.Close() + + return &responseJSON.Data, nil +} + +func (f *Fs) studIPUpdateFolder( + ctx context.Context, + folderID string, + name string, + parentFolderID string, +) error { + if ctx.Err() != nil { + return ctx.Err() + } + + Assert( + f != nil, + fmt.Sprintf( + "f must be not nil; f=%q", + f, + ), + ) + + if folderID == "" { + return errors.New("folderID is empty") + } + if name == "" && parentFolderID == "" { + return errors.New("name and parentFolderID are empty") + } + + URL := fmt.Sprintf("folders/%s", folderID) + apiName := "" + if name != "" { + apiName = f.opt.Enc.FromStandardName(name) + } + + data := map[string]any{ + "type": "folders", + } + if name != "" { + data["attributes"] = map[string]any{ + "name": apiName, + } + } + if parentFolderID != "" { + data["relationships"] = map[string]any{ + "parent": map[string]any{ + "data": map[string]any{ + "type": "folders", + "id": parentFolderID, + }, + }, + } + } + + body, err := json.Marshal(map[string]any{ + "data": data, + }) + if err != nil { + return err + } + + res, err := f.client.Call(ctx, &rest.Opts{ + Method: "PATCH", + Path: URL, + ContentType: "application/vnd.api+json", + Body: bytes.NewReader(body), + }) + if err != nil { + return err + } + defer res.Body.Close() + + return nil +} + func (f *Fs) studIPOpenFileContent( ctx context.Context, fileRefID string, diff --git a/backend/studip/responses.go b/backend/studip/responses.go index d328ec9..93fef28 100644 --- a/backend/studip/responses.go +++ b/backend/studip/responses.go @@ -30,12 +30,7 @@ type StudIPFoldersData struct { IsSubfolderAllowed bool `json:"is-subfolder-allowed"` } `json:"attributes"` Relationships struct { - Parent struct { - Data struct { - Type string `json:"type"` - ID string `json:"id"` - } `json:"data"` - } `json:"parent"` + Parent StudIPRelationship `json:"parent"` } `json:"relationships"` } @@ -70,6 +65,46 @@ type StudIPFiles struct { } `json:"data"` } +type StudIPResourceIdentifier struct { + Type string `json:"type"` + ID string `json:"id"` +} + +type StudIPRelationship struct { + Data StudIPResourceIdentifier `json:"data"` +} + +type StudIPNullableRelationship struct { + Data *StudIPResourceIdentifier `json:"data"` +} + +type StudIPFileRefData struct { + Type string `json:"type"` + ID string `json:"id"` + Attributes struct { + Name string `json:"name"` + Description string `json:"description"` + Mkdate time.Time `json:"mkdate"` + Chdate time.Time `json:"chdate"` + Downloads int `json:"downloads"` + Filesize int64 `json:"filesize"` + MimeType string `json:"mime-type"` + IsReadable bool `json:"is-readable"` + IsDownloadable bool `json:"is-downloadable"` + IsEditable bool `json:"is-editable"` + IsWritable bool `json:"is-writable"` + } `json:"attributes"` + Relationships struct { + File StudIPRelationship `json:"file"` + Parent StudIPRelationship `json:"parent"` + TermsOfUse StudIPNullableRelationship `json:"terms-of-use"` + } `json:"relationships"` +} + +type StudIPFileRef struct { + Data StudIPFileRefData `json:"data"` +} + type StudIPCourses struct { Data struct { Type string `json:"type"` |
