aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Tews <michael@tews.dev>2026-03-31 02:55:22 +0200
committerMichael Tews <michael@tews.dev>2026-03-31 04:00:05 +0200
commit74b0f535de0b021951744425f32e0182e40e6a9d (patch)
tree31d30a5be20a998d85e069cde3e870bfc08879b7
parent030cc6e5d9035dc8405bf28d6b3a96367b9a1400 (diff)
fix: failing integration test suite
-rw-r--r--backend/studip/filetree.go17
-rw-r--r--backend/studip/fs.go549
-rw-r--r--backend/studip/object.go71
-rw-r--r--backend/studip/requests.go90
4 files changed, 637 insertions, 90 deletions
diff --git a/backend/studip/filetree.go b/backend/studip/filetree.go
index 8f7c5c7..099bfdc 100644
--- a/backend/studip/filetree.go
+++ b/backend/studip/filetree.go
@@ -119,6 +119,9 @@ func (root *Node) GetNodeAtPath(path string) *Node {
found := false
for _, children := range currentNode.Children {
+ if children == nil {
+ continue
+ }
if strings.EqualFold(children.Name, pathSplit[0]) {
currentNode = children
pathSplit = pathSplit[1:]
@@ -147,7 +150,8 @@ func (ft *FileTree) ListEntries(fsys *Fs, dir string) (entries fs.DirEntries, er
return nil, fs.ErrorIsFile
}
- node := ft.relativeRoot.GetNodeAtPath(dir)
+ var node *Node
+ node = ft.relativeRoot.GetNodeAtPath(dir)
if node == nil {
return nil, fs.ErrorDirNotFound
}
@@ -157,13 +161,10 @@ func (ft *FileTree) ListEntries(fsys *Fs, dir string) (entries fs.DirEntries, er
}
for _, child := range node.Children {
- Assert(
- child != nil,
- fmt.Sprintf(
- "child node must be not nil; dir=%q child=%q",
- dir, child,
- ),
- )
+ if child == nil {
+ fs.Debugf(fsys, "ListEntries: skipping nil child dir=%q", dir)
+ continue
+ }
Assert(
child.ID != "",
diff --git a/backend/studip/fs.go b/backend/studip/fs.go
index ad5643c..5aaf392 100644
--- a/backend/studip/fs.go
+++ b/backend/studip/fs.go
@@ -12,6 +12,7 @@ import (
"slices"
"strings"
"sync"
+ "sync/atomic"
"time"
"github.com/rclone/rclone/fs"
@@ -29,9 +30,22 @@ type Fs struct {
opt *Options
client *rest.Client
// This is the path that rclone uses as the root
- relativeRootPath string
- ft FileTree
- mu sync.Mutex
+ relativeRootPath string
+ ft FileTree
+ treeGeneration uint64
+ treeRefreshGeneration uint64
+ activeMutations atomic.Int64
+ // mu guards in-memory file tree reads and writes.
+ mu sync.RWMutex
+}
+
+var fileTreeGenerations sync.Map
+var fileTreeSnapshots sync.Map
+var fileTreeMutationLocks sync.Map
+
+type fileTreeSnapshot struct {
+ generation uint64
+ root *Node
}
func NewFs(
@@ -49,6 +63,7 @@ func NewFs(
opt := new(Options)
if err := configstruct.Set(m, opt); err != nil {
fs.Debugf(name, "failed to parse backend config: %v", err)
+
return nil, err
}
@@ -84,11 +99,11 @@ func NewFs(
// ---- Request ----
if req != nil {
- b.WriteString(fmt.Sprintf("Request: %s %s\n", req.Method, req.URL.String()))
+ fmt.Fprintf(&b, "Request: %s %s\n", req.Method, req.URL.String())
b.WriteString("Request Headers:\n")
for k, v := range req.Header {
- b.WriteString(fmt.Sprintf(" %s: %v\n", k, v))
+ fmt.Fprintf(&b, " %s: %v\n", k, v)
}
if req.Body != nil {
@@ -107,11 +122,11 @@ func NewFs(
}
// ---- Response ----
- b.WriteString(fmt.Sprintf("Response Status: %s\n", resp.Status))
+ fmt.Fprintf(&b, "Response Status: %s\n", resp.Status)
b.WriteString("Response Headers:\n")
for k, v := range resp.Header {
- b.WriteString(fmt.Sprintf(" %s: %v\n", k, v))
+ fmt.Fprintf(&b, " %s: %v\n", k, v)
}
defer resp.Body.Close()
@@ -150,15 +165,22 @@ func NewFs(
fs.Debugf(f, "connection test successful")
fs.Debugf(f, "building course file tree")
- f.ft.root, err = f.GetCourseFileTree(ctx)
- if err != nil {
- return nil, err
+ f.ft.root = f.clonedSnapshotForCurrentGeneration()
+ if f.ft.root == nil {
+ f.ft.root, err = f.GetCourseFileTree(ctx)
+ if err != nil {
+ return nil, err
+ }
+ f.storeFileTreeSnapshot(f.ft.root, f.fileTreeGenerationCounter().Load())
}
fs.Debugf(f, "course file tree initialized")
if rootPath == "" {
f.ft.relativeRoot = f.ft.root
+ f.treeGeneration = f.fileTreeGenerationCounter().Load()
+ f.treeRefreshGeneration = f.treeGeneration
+
return f, nil
}
@@ -171,13 +193,283 @@ func NewFs(
if !f.ft.relativeRoot.IsDir {
f.ft.relativeRoot = f.ft.relativeRoot.Parent
f.relativeRootPath = dirPath(f.relativeRootPath)
+ f.treeGeneration = f.fileTreeGenerationCounter().Load()
+ f.treeRefreshGeneration = f.treeGeneration
+
return f, fs.ErrorIsFile
}
}
+ f.treeGeneration = f.fileTreeGenerationCounter().Load()
+ f.treeRefreshGeneration = f.treeGeneration
return f, nil
}
+func cloneNode(root *Node, parent *Node) *Node {
+ if root == nil {
+ return nil
+ }
+
+ cloned := &Node{
+ Parent: parent,
+ Name: root.Name,
+ Path: root.Path,
+ ID: root.ID,
+ IsReadable: root.IsReadable,
+ IsWritable: root.IsWritable,
+ IsDownloadable: root.IsDownloadable,
+ IsEditable: root.IsEditable,
+ IsSubfolderAllowed: root.IsSubfolderAllowed,
+ IsDir: root.IsDir,
+ ChDate: root.ChDate,
+ Size: root.Size,
+ ContentType: root.ContentType,
+ }
+
+ if len(root.Children) == 0 {
+ return cloned
+ }
+
+ cloned.Children = make([]*Node, 0, len(root.Children))
+ for _, child := range root.Children {
+ if child == nil {
+ cloned.Children = append(cloned.Children, nil)
+ continue
+ }
+ cloned.Children = append(cloned.Children, cloneNode(child, cloned))
+ }
+
+ return cloned
+}
+
+func (f *Fs) fileTreeKey() string {
+ return f.opt.BaseURL + "|" + f.opt.CourseID
+}
+
+func (f *Fs) fileTreeGenerationCounter() *atomic.Uint64 {
+ key := f.fileTreeKey()
+ counterAny, _ := fileTreeGenerations.LoadOrStore(key, &atomic.Uint64{})
+
+ return counterAny.(*atomic.Uint64)
+}
+
+func (f *Fs) fileTreeMutationLock() *sync.Mutex {
+ key := f.fileTreeKey()
+ lockAny, _ := fileTreeMutationLocks.LoadOrStore(key, &sync.Mutex{})
+
+ return lockAny.(*sync.Mutex)
+}
+
+func (f *Fs) markTreeCurrent(generation uint64) {
+ f.treeGeneration = generation
+ f.treeRefreshGeneration = generation
+}
+
+// Caller must hold f.mu.
+func (f *Fs) bumpTreeGenerationAndMarkCurrent() uint64 {
+ generation := f.fileTreeGenerationCounter().Add(1)
+ f.markTreeCurrent(generation)
+ f.storeCurrentFileTreeSnapshotLocked(generation)
+
+ return generation
+}
+
+func (f *Fs) clonedSnapshotForCurrentGeneration() *Node {
+ key := f.fileTreeKey()
+ snapshotAny, ok := fileTreeSnapshots.Load(key)
+ if !ok {
+ return nil
+ }
+
+ snapshot := snapshotAny.(*fileTreeSnapshot)
+ current := f.fileTreeGenerationCounter().Load()
+ if snapshot.generation != current || snapshot.root == nil {
+ return nil
+ }
+
+ return cloneNode(snapshot.root, nil)
+}
+
+func (f *Fs) storeFileTreeSnapshot(root *Node, generation uint64) {
+ if root == nil {
+ return
+ }
+
+ fileTreeSnapshots.Store(
+ f.fileTreeKey(),
+ &fileTreeSnapshot{
+ generation: generation,
+ root: cloneNode(root, nil),
+ },
+ )
+}
+
+func (f *Fs) storeCurrentFileTreeSnapshotLocked(generation uint64) {
+ if f.ft.root == nil {
+ return
+ }
+
+ f.storeFileTreeSnapshot(f.ft.root, generation)
+}
+
+func (f *Fs) beginMutation() {
+ f.activeMutations.Add(1)
+}
+
+func (f *Fs) endMutation() {
+ f.activeMutations.Add(-1)
+}
+
+func (f *Fs) hasActiveMutations() bool {
+ return f.activeMutations.Load() != 0
+}
+
+func (f *Fs) fileTreeNeedsRefresh() bool {
+ if f.hasActiveMutations() {
+ return false
+ }
+
+ current := f.fileTreeGenerationCounter().Load()
+ f.mu.RLock()
+ defer f.mu.RUnlock()
+
+ return f.treeGeneration != current || f.treeRefreshGeneration != f.treeGeneration
+}
+
+func (f *Fs) refreshFileTree(ctx context.Context) error {
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+
+ root := f.clonedSnapshotForCurrentGeneration()
+ if root == nil {
+ var err error
+ root, err = f.GetCourseFileTree(ctx)
+ if err != nil {
+ return err
+ }
+ }
+
+ f.mu.Lock()
+ defer f.mu.Unlock()
+
+ f.ft.root = root
+ generation := f.fileTreeGenerationCounter().Load()
+ f.treeGeneration = generation
+ f.treeRefreshGeneration = generation
+ f.ft.relativeRoot = nil
+ if f.relativeRootPath == "" {
+ f.ft.relativeRoot = root
+ f.storeCurrentFileTreeSnapshotLocked(generation)
+ return nil
+ }
+
+ f.ft.relativeRoot = root.GetNodeAtPath(f.relativeRootPath)
+ if f.ft.relativeRoot != nil && !f.ft.relativeRoot.IsDir {
+ f.ft.relativeRoot = f.ft.relativeRoot.Parent
+ }
+
+ f.storeCurrentFileTreeSnapshotLocked(generation)
+
+ return nil
+}
+
+func (f *Fs) ensureCurrentFileTree(ctx context.Context) error {
+ if !f.fileTreeNeedsRefresh() {
+ return nil
+ }
+ return f.refreshFileTree(ctx)
+}
+
+func (f *Fs) sameCourse(other *Fs) bool {
+ return f != nil &&
+ other != nil &&
+ f.opt != nil &&
+ other.opt != nil &&
+ f.opt.BaseURL == other.opt.BaseURL &&
+ f.opt.CourseID == other.opt.CourseID
+}
+
+func beginMutations(fss ...*Fs) func() {
+ seen := make(map[*Fs]struct{}, len(fss))
+ order := make([]*Fs, 0, len(fss))
+
+ for _, fsys := range fss {
+ if fsys == nil {
+ continue
+ }
+ if _, ok := seen[fsys]; ok {
+ continue
+ }
+ seen[fsys] = struct{}{}
+ fsys.beginMutation()
+ order = append(order, fsys)
+ }
+
+ return func() {
+ for i := len(order) - 1; i >= 0; i-- {
+ order[i].endMutation()
+ }
+ }
+}
+
+func lockMutationCourses(fss ...*Fs) func() {
+ type courseLock struct {
+ key string
+ mu *sync.Mutex
+ }
+
+ seen := make(map[string]struct{}, len(fss))
+ locks := make([]courseLock, 0, len(fss))
+
+ for _, fsys := range fss {
+ if fsys == nil || fsys.opt == nil {
+ continue
+ }
+ key := fsys.fileTreeKey()
+ if _, ok := seen[key]; ok {
+ continue
+ }
+ seen[key] = struct{}{}
+ locks = append(locks, courseLock{
+ key: key,
+ mu: fsys.fileTreeMutationLock(),
+ })
+ }
+
+ slices.SortFunc(locks, func(a, b courseLock) int {
+ return strings.Compare(a.key, b.key)
+ })
+
+ for _, lock := range locks {
+ lock.mu.Lock()
+ }
+
+ return func() {
+ for i := len(locks) - 1; i >= 0; i-- {
+ locks[i].mu.Unlock()
+ }
+ }
+}
+
+func updateSubtreePaths(node *Node) {
+ if node == nil {
+ return
+ }
+
+ if node.Parent != nil {
+ node.Path = joinPath(node.Parent.Path, node.Name)
+ }
+
+ for _, child := range node.Children {
+ if child == nil {
+ continue
+ }
+ updateSubtreePaths(child)
+ }
+}
+
+
func (f *Fs) GetCourseFileTree(
ctx context.Context,
) (*Node, error) {
@@ -415,6 +707,16 @@ func (f *Fs) Put(
return nil, fmt.Errorf("invalid remote path %q", remotePath)
}
+ unlockCourses := lockMutationCourses(f)
+ defer unlockCourses()
+
+ if err := f.ensureCurrentFileTree(ctx); err != nil {
+ return nil, err
+ }
+
+ f.beginMutation()
+ defer f.endMutation()
+
existingAny, err := f.NewObject(ctx, remotePath)
if err == nil {
existing, ok := existingAny.(*Object)
@@ -447,6 +749,8 @@ func (f *Fs) Put(
existing.size = src.Size()
existing.modTime = src.ModTime(ctx)
existing.contentType = fs.MimeType(ctx, src)
+
+ f.mu.Lock()
if f.ft.relativeRoot != nil {
if node := f.ft.relativeRoot.GetNodeAtPath(remotePath); node != nil && !node.IsDir {
node.ID = existing.id
@@ -455,6 +759,8 @@ func (f *Fs) Put(
node.ContentType = existing.contentType
}
}
+ f.bumpTreeGenerationAndMarkCurrent()
+ f.mu.Unlock()
err = existing.SetTermsOfUse(ctx, f.opt.License)
if err != nil {
@@ -488,12 +794,8 @@ func (f *Fs) Put(
parentDir := dirPath(remotePath)
cleanRoot := cleanPath(f.relativeRootPath)
- parentDirForCreation := parentDir
- if f.ft.relativeRoot == nil {
- parentDirForCreation = joinPath(cleanRoot, parentDir)
- }
- directoryNode, err := f.CreateParentDirectories(ctx, parentDirForCreation)
+ directoryNode, err := f.CreateParentDirectories(ctx, joinPath(cleanRoot, parentDir))
if err != nil {
return nil, err
}
@@ -529,6 +831,8 @@ func (f *Fs) Put(
}
filename := basePath(remotePath)
+ f.mu.Lock()
+ defer f.mu.Unlock()
updatedNode := false
for _, child := range directoryNode.Children {
if child == nil || child.IsDir || !strings.EqualFold(child.Name, filename) {
@@ -564,6 +868,7 @@ func (f *Fs) Put(
ContentType: object.contentType,
})
}
+ f.bumpTreeGenerationAndMarkCurrent()
return object, nil
}
@@ -581,17 +886,27 @@ func (f *Fs) Mkdir(ctx context.Context, dir string) error {
),
)
- fs.Infof(f, "Mkdir f.root=%q dir=%q", f.relativeRootPath, dir)
+ unlockCourses := lockMutationCourses(f)
+ defer unlockCourses()
+ if err := f.ensureCurrentFileTree(ctx); err != nil {
+ return err
+ }
+
+ f.beginMutation()
+ defer f.endMutation()
var parentNode *Node
var dirname string
var err error
// creating relativeRoot
if dir == "" {
- if f.ft.relativeRoot == nil {
+ f.mu.RLock()
+ relativeRootReady := f.ft.relativeRoot != nil
+ f.mu.RUnlock()
+ if !relativeRootReady {
fs.Debugf(f, "Mkdir: rootNode nil, creating parent chain for %q", dirPath(f.relativeRootPath))
- parentNode, err = f.CreateParentDirectories(ctx, dirPath(f.relativeRootPath))
+ parentNode, err = f.CreateParentDirectories(ctx, dirPath(cleanPath(f.relativeRootPath)))
if err != nil {
return err
}
@@ -602,12 +917,14 @@ func (f *Fs) Mkdir(ctx context.Context, dir string) error {
} else {
// creating dir inside relativeRoot
dirname = basePath(dir)
+ f.mu.RLock()
if f.ft.relativeRoot != nil {
parentNode = f.ft.relativeRoot.GetNodeAtPath(dirPath(dir))
}
+ f.mu.RUnlock()
if parentNode == nil {
fs.Debugf(f, "Mkdir: parent missing for %q, creating chain", dir)
- parentNode, err = f.CreateParentDirectories(ctx, dirPath(dir))
+ parentNode, err = f.CreateParentDirectories(ctx, joinPath(cleanPath(f.relativeRootPath), dirPath(dir)))
if err != nil {
return err
}
@@ -642,10 +959,12 @@ func (f *Fs) Mkdir(ctx context.Context, dir string) error {
dirname,
)
- // is this needed?
+ f.mu.RLock()
if f.findDirectoryNodeByName(parentNode, dirname) != nil {
+ f.mu.RUnlock()
return nil
}
+ f.mu.RUnlock()
fs.Debugf(f, "Mkdir: creating directory %q under parent id=%q", dirname, parentNode.ID)
apiDirname := f.opt.Enc.FromStandardName(dirname)
@@ -674,7 +993,8 @@ func (f *Fs) Mkdir(ctx context.Context, dir string) error {
Size: -1,
}
- // is this needed?
+ f.mu.Lock()
+ defer f.mu.Unlock()
if f.findDirectoryNodeByName(parentNode, dirname) != nil {
return nil
}
@@ -682,6 +1002,7 @@ func (f *Fs) Mkdir(ctx context.Context, dir string) error {
parentNode.Children = append(parentNode.Children, createdDirectoryNode)
f.updateRelativeRootFromTree()
+ f.bumpTreeGenerationAndMarkCurrent()
return nil
}
@@ -712,7 +1033,7 @@ func (f *Fs) findDirectoryNodeByName(parentNode *Node, name string) *Node {
)
for _, child := range parentNode.Children {
- if child.IsDir && strings.EqualFold(child.Name, name) {
+ if child != nil && child.IsDir && strings.EqualFold(child.Name, name) {
return child
}
}
@@ -767,10 +1088,11 @@ func (f *Fs) findDirectoryByName(
return StudIPFoldersData{}, fs.ErrorDirNotFound
}
-// This is for creating the parent directories for a non existing directory
+// CreateParentDirectories creates missing directory segments for a path relative
+// to the course root, regardless of the current relative root state.
func (f *Fs) CreateParentDirectories(
ctx context.Context,
- dir string,
+ targetPath string,
) (*Node, error) {
if ctx.Err() != nil {
return nil, ctx.Err()
@@ -787,12 +1109,7 @@ func (f *Fs) CreateParentDirectories(
f.mu.Lock()
defer f.mu.Unlock()
- var targetPath string
- if f.ft.relativeRoot != nil {
- targetPath = joinPath(f.relativeRootPath, dir)
- } else {
- targetPath = dir
- }
+ targetPath = cleanPath(targetPath)
fs.Debugf(f, "CreateParentDirectories: normalized targetPath=%q", targetPath)
targetNode := f.ft.root.GetNodeAtPath(targetPath)
@@ -815,6 +1132,7 @@ func (f *Fs) CreateParentDirectories(
stack := NewStack[string]()
currentPath := targetPath
+ createdAny := false
for {
candidate := f.ft.root.GetNodeAtPath(currentPath)
@@ -891,6 +1209,7 @@ func (f *Fs) CreateParentDirectories(
targetNode.Children = append(targetNode.Children, createdNode)
targetNode = createdNode
+ createdAny = true
}
fs.Debugf(
f,
@@ -899,6 +1218,9 @@ func (f *Fs) CreateParentDirectories(
targetNode.ID,
)
f.updateRelativeRootFromTree()
+ if createdAny {
+ f.bumpTreeGenerationAndMarkCurrent()
+ }
return targetNode, nil
}
@@ -952,42 +1274,81 @@ func (f *Fs) Rmdir(ctx context.Context, dir string) error {
),
)
- if f.ft.relativeRoot == nil {
- return fs.ErrorDirNotFound
- }
+ unlockCourses := lockMutationCourses(f)
+ defer unlockCourses()
- node := f.ft.relativeRoot.GetNodeAtPath(dir)
- if node == nil {
- return fs.ErrorDirNotFound
+ if f.fileTreeNeedsRefresh() {
+ if err := f.refreshFileTree(ctx); err != nil {
+ fs.Debugf(f, "Rmdir: refresh before lookup failed dir=%q err=%v", dir, err)
+ }
}
- if !node.IsEditable {
- return fs.ErrorPermissionDenied
- }
+ f.beginMutation()
+ defer f.endMutation()
+
+ lookupNode := func() (*Node, error) {
+ f.mu.RLock()
+ defer f.mu.RUnlock()
+
+ if f.ft.relativeRoot == nil {
+ return nil, fs.ErrorDirNotFound
+ }
+
+ node := f.ft.relativeRoot.GetNodeAtPath(dir)
+ if node == nil {
+ return nil, fs.ErrorDirNotFound
+ }
- // if Directory is root
- if node.Parent == nil && node.Name == f.ft.root.Name && node.Path == f.ft.root.Path {
- return fs.ErrorCantPurge
+ if !node.IsEditable {
+ return nil, fs.ErrorPermissionDenied
+ }
+
+ // if Directory is root
+ if node.Parent == nil && node.Name == f.ft.root.Name && node.Path == f.ft.root.Path {
+ return nil, fs.ErrorCantPurge
+ }
+
+ return node, nil
}
+ node, err := lookupNode()
+ if err != nil {
+ return err
+ }
if len(node.Children) > 0 {
- return fs.ErrorDirectoryNotEmpty
+ if refreshErr := f.refreshFileTree(ctx); refreshErr != nil {
+ fs.Debugf(f, "Rmdir: refresh before empty check failed dir=%q err=%v", dir, refreshErr)
+ } else {
+ node, err = lookupNode()
+ if err != nil {
+ return err
+ }
+ }
+
+ if len(node.Children) > 0 {
+ return fs.ErrorDirectoryNotEmpty
+ }
}
- err := f.studIPDeleteFolder(ctx, node.ID)
+ err = f.studIPDeleteFolder(ctx, node.ID)
if err != nil {
return err
}
+ f.mu.Lock()
+ defer f.mu.Unlock()
// if the deleted node was the relativeRootPath we have to nil it
- if f.ft.relativeRoot.ID == node.ID {
+ if f.ft.relativeRoot != nil && f.ft.relativeRoot.ID == node.ID {
f.ft.relativeRoot = nil
}
- index := slices.Index(node.Parent.Children, node)
- if index >= 0 {
- node.Parent.Children = slices.Delete(node.Parent.Children, index, index+1)
+ if node.Parent != nil {
+ index := slices.Index(node.Parent.Children, node)
+ if index >= 0 {
+ node.Parent.Children = slices.Delete(node.Parent.Children, index, index+1)
+ }
}
+ f.bumpTreeGenerationAndMarkCurrent()
return nil
}
@@ -1057,11 +1418,6 @@ func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
)
fs.Debugf(f, "NewObject: start remote=%q", remote)
- if f == nil || f.ft.relativeRoot == nil {
- fs.Debugf(f, "NewObject: relative root is not available")
- return nil, fs.ErrorObjectNotFound
- }
-
remote = cleanPath(remote)
fs.Debugf(f, "NewObject: normalized remote=%q", remote)
if remote == "" {
@@ -1069,30 +1425,62 @@ func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
return nil, fs.ErrorObjectNotFound
}
- node := f.ft.relativeRoot.GetNodeAtPath(remote)
- if node == nil || node.IsDir || node.ID == "" {
- if node == nil {
+ if f.fileTreeNeedsRefresh() {
+ if err := f.refreshFileTree(ctx); err != nil {
+ fs.Debugf(f, "NewObject: refresh before lookup failed remote=%q err=%v", remote, err)
+ }
+ }
+
+ var (
+ object *Object
+ relativeRootMissing bool
+ nodeMissing bool
+ nodeIsDir bool
+ nodeIDMissing bool
+ )
+
+ f.mu.RLock()
+ if f.ft.relativeRoot == nil {
+ relativeRootMissing = true
+ } else {
+ node := f.ft.relativeRoot.GetNodeAtPath(remote)
+ switch {
+ case node == nil:
+ nodeMissing = true
+ case node.IsDir:
+ nodeIsDir = true
+ case node.ID == "":
+ nodeIDMissing = true
+ default:
+ object = &Object{
+ fs: f,
+ remote: remote,
+ id: node.ID,
+ size: node.Size,
+ isReadable: node.IsReadable,
+ isEditable: node.IsEditable,
+ isWritable: node.IsWritable,
+ IsDownloadable: node.IsDownloadable,
+ contentType: node.ContentType,
+ modTime: node.ChDate,
+ }
+ }
+ }
+ f.mu.RUnlock()
+
+ if object == nil {
+ if relativeRootMissing {
+ fs.Debugf(f, "NewObject: relative root is not available for %q", remote)
+ } else if nodeMissing {
fs.Debugf(f, "NewObject: node not found for %q", remote)
- } else if node.IsDir {
+ } else if nodeIsDir {
fs.Debugf(f, "NewObject: path %q is a directory", remote)
- } else {
+ } else if nodeIDMissing {
fs.Debugf(f, "NewObject: node for %q has empty id", remote)
}
return nil, fs.ErrorObjectNotFound
}
- object := &Object{
- fs: f,
- remote: remote,
- id: node.ID,
- size: node.Size,
- isReadable: node.IsReadable,
- isEditable: node.IsEditable,
- isWritable: node.IsWritable,
- IsDownloadable: node.IsDownloadable,
- contentType: node.ContentType,
- modTime: node.ChDate,
- }
fs.Debugf(
f,
"NewObject: resolved remote=%q id=%q size=%d contentType=%q",
@@ -1123,7 +1511,26 @@ func (f *Fs) List(
fs.Debugf(f, "List: start dir=%q rootPath=%q", dir, f.relativeRootPath)
- entries, err = f.ft.ListEntries(f, dir)
+ if f.fileTreeNeedsRefresh() {
+ if err := f.refreshFileTree(ctx); err != nil {
+ fs.Debugf(f, "List: refresh before lookup failed dir=%q err=%v", dir, err)
+ }
+ }
+
+ fixCase := fs.GetConfig(ctx).FixCase
+ listEntries := func() (fs.DirEntries, error) {
+ f.mu.RLock()
+ defer f.mu.RUnlock()
+ if fixCase {
+ entries, err := f.ft.ListEntries(f, dir)
+ if err == nil || !errors.Is(err, fs.ErrorDirNotFound) {
+ return entries, err
+ }
+ }
+ return f.ft.ListEntries(f, dir)
+ }
+
+ entries, err = listEntries()
if err != nil {
fs.Debugf(f, "List: failed dir=%q err=%v", dir, err)
return nil, err
diff --git a/backend/studip/object.go b/backend/studip/object.go
index cc569b8..f79ff61 100644
--- a/backend/studip/object.go
+++ b/backend/studip/object.go
@@ -159,6 +159,16 @@ func (o *Object) Update(
srcRemote := src.Remote()
fs.Debugf(o.fs, "Object.Update: start srcRemote=%q fields={%s}", srcRemote, o.fieldsForLog())
+ unlockCourses := lockMutationCourses(o.fs)
+ defer unlockCourses()
+
+ if err := o.fs.ensureCurrentFileTree(ctx); err != nil {
+ return err
+ }
+
+ o.fs.beginMutation()
+ defer o.fs.endMutation()
+
if !o.isEditable || !o.isWritable {
fs.Debugf(o.fs, "Object.Update: permission denied srcRemote=%q fields={%s}", srcRemote, o.fieldsForLog())
return fs.ErrorPermissionDenied
@@ -199,6 +209,7 @@ func (o *Object) Update(
o.modTime = src.ModTime(ctx)
o.contentType = fs.MimeType(ctx, src)
+ o.fs.mu.Lock()
if o.fs.ft.relativeRoot != nil {
if node := o.fs.ft.relativeRoot.GetNodeAtPath(o.remote); node != nil && !node.IsDir {
node.ID = o.id
@@ -207,6 +218,8 @@ func (o *Object) Update(
node.ContentType = o.contentType
}
}
+ o.fs.bumpTreeGenerationAndMarkCurrent()
+ o.fs.mu.Unlock()
err = o.SetTermsOfUse(ctx, o.fs.opt.License)
if err != nil {
@@ -222,7 +235,7 @@ func (o *Object) recreateForSizeChangingUpdate(
in io.Reader,
src fs.ObjectInfo,
) (string, error) {
- parentNode, err := o.fs.CreateParentDirectories(ctx, dirPath(o.remote))
+ parentNode, err := o.fs.CreateParentDirectories(ctx, joinPath(cleanPath(o.fs.relativeRootPath), dirPath(o.remote)))
if err != nil {
return "", err
}
@@ -254,6 +267,7 @@ func (o *Object) recreateForSizeChangingUpdate(
o.modTime = src.ModTime(ctx)
o.contentType = fs.MimeType(ctx, src)
+ o.fs.mu.Lock()
if o.fs.ft.relativeRoot != nil {
if node := o.fs.ft.relativeRoot.GetNodeAtPath(o.remote); node != nil && !node.IsDir {
node.ID = o.id
@@ -262,6 +276,8 @@ func (o *Object) recreateForSizeChangingUpdate(
node.ContentType = o.contentType
}
}
+ o.fs.bumpTreeGenerationAndMarkCurrent()
+ o.fs.mu.Unlock()
err = o.SetTermsOfUse(ctx, o.fs.opt.License)
if err != nil {
@@ -356,6 +372,16 @@ func (o *Object) Remove(ctx context.Context) error {
fs.Debugf(o.fs, "Object.Remove: start fields={%s}", o.fieldsForLog())
+ unlockCourses := lockMutationCourses(o.fs)
+ defer unlockCourses()
+
+ if err := o.fs.ensureCurrentFileTree(ctx); err != nil {
+ return err
+ }
+
+ o.fs.beginMutation()
+ defer o.fs.endMutation()
+
if !o.isEditable && !o.isWritable {
fs.Debugf(o.fs, "Object.Remove: permission denied fields={%s}", o.fieldsForLog())
return fs.ErrorPermissionDenied
@@ -367,22 +393,45 @@ func (o *Object) Remove(ctx context.Context) error {
return err
}
- var parent *Node
- if o.fs.ft.relativeRoot != nil {
- parentDir := dirPath(o.remote)
- parent = o.fs.ft.relativeRoot.GetNodeAtPath(parentDir)
+ o.fs.mu.Lock()
+ defer o.fs.mu.Unlock()
+ removed := false
+ if o.fs.ft.root != nil {
+ absoluteRemote := joinPath(cleanPath(o.fs.relativeRootPath), o.remote)
+ node := o.fs.ft.root.GetNodeAtPath(absoluteRemote)
+ if node == nil {
+ node = o.fs.ft.root.GetNodeAtPath(absoluteRemote)
+ }
+ if node != nil && node.Parent != nil {
+ index := slices.Index(node.Parent.Children, node)
+ if index >= 0 {
+ node.Parent.Children = slices.Delete(node.Parent.Children, index, index+1)
+ removed = true
+ }
+ }
}
- if parent != nil {
- filename := basePath(o.remote)
- for i, child := range parent.Children {
- if child != nil && !child.IsDir && strings.EqualFold(child.Name, filename) {
- parent.Children = slices.Delete(parent.Children, i, i+1)
- break
+ if !removed && o.fs.ft.relativeRoot != nil {
+ parentDir := dirPath(o.remote)
+ parent := o.fs.ft.relativeRoot.GetNodeAtPath(parentDir)
+ if parent != nil {
+ filename := basePath(o.remote)
+ for i, child := range parent.Children {
+ if child != nil && !child.IsDir && strings.EqualFold(child.Name, filename) {
+ parent.Children = slices.Delete(parent.Children, i, i+1)
+ removed = true
+ break
+ }
}
}
}
+ if removed {
+ o.fs.bumpTreeGenerationAndMarkCurrent()
+ } else {
+ o.fs.fileTreeGenerationCounter().Add(1)
+ }
+
fs.Debugf(o.fs, "Object.Remove: success fields={%s}", o.fieldsForLog())
return nil
diff --git a/backend/studip/requests.go b/backend/studip/requests.go
index afb5c04..5a637f8 100644
--- a/backend/studip/requests.go
+++ b/backend/studip/requests.go
@@ -9,6 +9,7 @@ import (
"io"
"net/http"
"net/url"
+ "strconv"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/lib/rest"
@@ -102,6 +103,24 @@ func (f *Fs) studIPMkDir(
return nil
}
+func pagedPath(basePath string, offset, limit int) string {
+ values := url.Values{}
+ values.Set("page[offset]", strconv.Itoa(offset))
+ values.Set("page[limit]", strconv.Itoa(limit))
+ return basePath + "?" + values.Encode()
+}
+
+func nextPageLimit(limit, loaded int) int {
+ switch {
+ case limit > 0:
+ return limit
+ case loaded > 0:
+ return loaded
+ default:
+ return 30
+ }
+}
+
func (f *Fs) studIPGetFoldersOfFolder(
ctx context.Context,
folderID string,
@@ -132,6 +151,27 @@ func (f *Fs) studIPGetFoldersOfFolder(
}
defer res.Body.Close()
+ for len(responseJSON.Data) < responseJSON.Meta.Page.Total {
+ offset := len(responseJSON.Data)
+ limit := nextPageLimit(responseJSON.Meta.Page.Limit, len(responseJSON.Data))
+ page := &StudIPFolders{}
+ res, err := f.client.CallJSON(
+ ctx,
+ &rest.Opts{Method: "GET", Path: pagedPath(URL, offset, limit)},
+ nil,
+ page,
+ )
+ if err != nil {
+ return nil, err
+ }
+ res.Body.Close()
+ if len(page.Data) == 0 {
+ break
+ }
+ responseJSON.Data = append(responseJSON.Data, page.Data...)
+ responseJSON.Meta = page.Meta
+ }
+
return responseJSON, nil
}
@@ -165,6 +205,28 @@ func (f *Fs) studIPGetFilesOfFolder(
}
defer res.Body.Close()
+ for len(responseJSON.Data) < responseJSON.Meta.Page.Total {
+ offset := len(responseJSON.Data)
+ limit := nextPageLimit(responseJSON.Meta.Page.Limit, len(responseJSON.Data))
+ page := &StudIPFiles{}
+ res, err := f.client.CallJSON(
+ ctx,
+ &rest.Opts{Method: "GET", Path: pagedPath(URL, offset, limit)},
+ nil,
+ page,
+ )
+ if err != nil {
+ return nil, err
+ }
+ res.Body.Close()
+ if len(page.Data) == 0 {
+ break
+ }
+ responseJSON.Data = append(responseJSON.Data, page.Data...)
+ responseJSON.Meta = page.Meta
+ responseJSON.Links = page.Links
+ }
+
return responseJSON, nil
}
@@ -195,6 +257,27 @@ func (f *Fs) studIPGetCourseFolders(ctx context.Context) (*StudIPFolders, error)
}
defer res.Body.Close()
+ for len(responseJSON.Data) < responseJSON.Meta.Page.Total {
+ offset := len(responseJSON.Data)
+ limit := nextPageLimit(responseJSON.Meta.Page.Limit, len(responseJSON.Data))
+ page := &StudIPFolders{}
+ res, err := f.client.CallJSON(
+ ctx,
+ &rest.Opts{Method: "GET", Path: pagedPath(URL, offset, limit)},
+ nil,
+ page,
+ )
+ if err != nil {
+ return nil, err
+ }
+ res.Body.Close()
+ if len(page.Data) == 0 {
+ break
+ }
+ responseJSON.Data = append(responseJSON.Data, page.Data...)
+ responseJSON.Meta = page.Meta
+ }
+
return responseJSON, nil
}
@@ -569,6 +652,13 @@ func (f *Fs) studIPDeleteFile(ctx context.Context, fileRefID string) error {
Path: URL,
})
if err != nil {
+ if res != nil {
+ defer res.Body.Close()
+ if res.StatusCode == http.StatusNotFound {
+ fs.Debugf(f, "studIPDeleteFile: ignoring missing file-ref id=%q", fileRefID)
+ return nil
+ }
+ }
return err
}
defer res.Body.Close()