aboutsummaryrefslogtreecommitdiff
path: root/backend/studip
diff options
context:
space:
mode:
Diffstat (limited to 'backend/studip')
-rw-r--r--backend/studip/fs.go290
-rw-r--r--backend/studip/requests.go283
-rw-r--r--backend/studip/responses.go47
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"`