aboutsummaryrefslogtreecommitdiff
path: root/backend/studip/object.go
diff options
context:
space:
mode:
Diffstat (limited to 'backend/studip/object.go')
-rw-r--r--backend/studip/object.go388
1 files changed, 388 insertions, 0 deletions
diff --git a/backend/studip/object.go b/backend/studip/object.go
new file mode 100644
index 0000000..a6b8e94
--- /dev/null
+++ b/backend/studip/object.go
@@ -0,0 +1,388 @@
+package studip
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/rclone/rclone/fs"
+ "github.com/rclone/rclone/fs/hash"
+)
+
+type Object struct {
+ fs *Fs
+ remote string
+ id string
+ size int64
+ isReadable bool
+ isEditable bool
+ isWritable bool
+ IsDownloadable bool
+ contentType string
+ modTime time.Time
+}
+
+func (o *Object) fieldsForLog() string {
+ if o == nil {
+ return "<nil>"
+ }
+
+ return fmt.Sprintf(
+ "remote=%q id=%q size=%d isReadable=%t isEditable=%t isWritable=%t isDownloadable=%t contentType=%q modTime=%q",
+ o.remote,
+ o.id,
+ o.size,
+ o.isReadable,
+ o.isEditable,
+ o.isWritable,
+ o.IsDownloadable,
+ o.contentType,
+ o.modTime.Format(time.RFC3339Nano),
+ )
+}
+
+func (o *Object) Fs() fs.Info {
+ return o.fs
+}
+
+func (o *Object) String() string {
+ if o == nil {
+ return "<nil>"
+ }
+ return o.remote
+}
+
+func (o *Object) Remote() string {
+ return o.remote
+}
+
+func (o *Object) Hash(ctx context.Context, r hash.Type) (string, error) {
+ return "", hash.ErrUnsupported
+}
+
+func (o *Object) Size() int64 {
+ return o.size
+}
+
+// ModTime returns the modification time of the remote http file
+func (o *Object) ModTime(ctx context.Context) time.Time {
+ return o.modTime
+}
+
+func (o *Object) SetModTime(ctx context.Context, t time.Time) error {
+ o.modTime = t
+
+ return nil
+}
+func (o *Object) MimeType(ctx context.Context) string { return "" }
+
+func (o *Object) Open(
+ ctx context.Context,
+ options ...fs.OpenOption,
+) (io.ReadCloser, error) {
+ if o == nil {
+ return nil, fmt.Errorf("object is nil")
+ }
+ if o.fs == nil {
+ return nil, fmt.Errorf("object fs is nil")
+ }
+
+ fs.Debugf(o.fs, "Object.Open: start fields={%s} options=%d", o.fieldsForLog(), len(options))
+ if ctx.Err() != nil {
+ fs.Debugf(o.fs, "Object.Open: context canceled remote=%q err=%v", o.remote, ctx.Err())
+ return nil, ctx.Err()
+ }
+
+ if !o.isReadable && !o.IsDownloadable {
+ fs.Debugf(o.fs, "Object.Open: permission denied fields={%s}", o.fieldsForLog())
+ return nil, fs.ErrorPermissionDenied
+ }
+
+ rc, err := o.fs.studIPOpenFileContent(ctx, o.id, options...)
+ if err != nil {
+ fs.Debugf(o.fs, "Object.Open: failed fields={%s} err=%v", o.fieldsForLog(), err)
+ return nil, err
+ }
+ fs.Debugf(o.fs, "Object.Open: success fields={%s}", o.fieldsForLog())
+ return rc, nil
+}
+
+func (o *Object) Storable() bool {
+ return true
+}
+
+func (o *Object) Update(
+ ctx context.Context,
+ in io.Reader,
+ src fs.ObjectInfo,
+ options ...fs.OpenOption,
+) error {
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+
+ Assert(
+ o != nil,
+ fmt.Sprintf(
+ "o must be not nil; o=%q",
+ o,
+ ),
+ )
+
+ Assert(
+ o.fs != nil,
+ fmt.Sprintf(
+ "o.fs must be not nil; o.fs=%q",
+ o.fs,
+ ),
+ )
+
+ Assert(
+ in != nil,
+ fmt.Sprintf(
+ "in must be not nil; in=%q",
+ in,
+ ),
+ )
+
+ Assert(
+ src != nil,
+ fmt.Sprintf(
+ "src must be not nil; src=%q",
+ src,
+ ),
+ )
+
+ srcRemote := src.Remote()
+ fs.Debugf(o.fs, "Object.Update: start srcRemote=%q fields={%s}", srcRemote, o.fieldsForLog())
+
+ if !o.isEditable || !o.isWritable {
+ fs.Debugf(o.fs, "Object.Update: permission denied srcRemote=%q fields={%s}", srcRemote, o.fieldsForLog())
+ return fs.ErrorPermissionDenied
+ }
+
+ if o.id == "" {
+ fs.Debugf(o.fs, "Object.Update: missing file-ref id for srcRemote=%q", srcRemote)
+ return fmt.Errorf("cannot update %q: object id is empty", srcRemote)
+ }
+
+ // This weird branch fixed a case where sometimes a file update get's truncated to it's size instead of updating the size
+ sizeChanged := src.Size() >= 0 && o.size >= 0 && src.Size() != o.size
+ var location string
+ var err error
+ if sizeChanged {
+ fs.Debugf(
+ o.fs,
+ "Object.Update: size changed old=%d new=%d remote=%q; recreating file",
+ o.size,
+ src.Size(),
+ o.remote,
+ )
+ location, err = o.recreateForSizeChangingUpdate(ctx, in, src)
+ } else {
+ location, err = o.fs.studIPUpdateFileContent(ctx, o.id, in, basePath(o.remote), src.Size())
+ }
+ if err != nil {
+ fs.Debugf(o.fs, "Object.Update: upload phase failed srcRemote=%q fields={%s} err=%v", srcRemote, o.fieldsForLog(), err)
+ return err
+ }
+
+ o.id, err = fileRefIDFromLocation(location)
+ if err != nil {
+ return err
+ }
+
+ o.size = src.Size()
+ o.modTime = src.ModTime(ctx)
+ o.contentType = fs.MimeType(ctx, src)
+
+ if o.fs.ft.relativeRoot != nil {
+ if node := o.fs.ft.relativeRoot.GetNodeAtPath(o.remote); node != nil && !node.IsDir {
+ node.ID = o.id
+ node.Size = o.size
+ node.ChDate = o.modTime
+ node.ContentType = o.contentType
+ }
+ }
+
+ err = o.SetTermsOfUse(ctx, o.fs.opt.License)
+ if err != nil {
+ return err
+ }
+
+ fs.Debugf(o.fs, "Object.Update: success srcRemote=%q location=%q fields={%s}", srcRemote, location, o.fieldsForLog())
+ return nil
+}
+
+func (o *Object) recreateForSizeChangingUpdate(
+ ctx context.Context,
+ in io.Reader,
+ src fs.ObjectInfo,
+) (string, error) {
+ parentNode, err := o.fs.CreateParentDirectories(ctx, dirPath(o.remote))
+ if err != nil {
+ return "", err
+ }
+ if parentNode == nil || !parentNode.IsDir || parentNode.ID == "" {
+ return "", fmt.Errorf("failed to resolve parent directory for %q", o.remote)
+ }
+
+ if err = o.fs.studIPDeleteFile(ctx, o.id); err != nil {
+ return "", err
+ }
+
+ location, err := o.fs.studIPCreateFileContent(
+ ctx,
+ parentNode.ID,
+ in,
+ basePath(o.remote),
+ src.Size(),
+ )
+ if err != nil {
+ return location, err
+ }
+
+ newID, err := fileRefIDFromLocation(location)
+ if err != nil {
+ return location, err
+ }
+ o.id = newID
+ o.size = src.Size()
+ o.modTime = src.ModTime(ctx)
+ o.contentType = fs.MimeType(ctx, src)
+
+ if o.fs.ft.relativeRoot != nil {
+ if node := o.fs.ft.relativeRoot.GetNodeAtPath(o.remote); node != nil && !node.IsDir {
+ node.ID = o.id
+ node.Size = o.size
+ node.ChDate = o.modTime
+ node.ContentType = o.contentType
+ }
+ }
+
+ err = o.SetTermsOfUse(ctx, o.fs.opt.License)
+ if err != nil {
+ return location, err
+ }
+
+ return location, nil
+}
+
+func (o *Object) SetTermsOfUse(
+ ctx context.Context,
+ licenseID string,
+) error {
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+
+ Assert(
+ o != nil,
+ fmt.Sprintf(
+ "o must be not nil; o=%q",
+ o,
+ ),
+ )
+
+ Assert(
+ o.fs != nil,
+ fmt.Sprintf(
+ "o.fs must be not nil; o.fs=%q",
+ o.fs,
+ ),
+ )
+
+ Assert(
+ o.id != "",
+ fmt.Sprintf(
+ "o.id must be not empty; o.id=%q",
+ o.id,
+ ),
+ )
+
+ fs.Debugf(o.fs, "Object.SetTermsOfUse: start license=%q fields={%s}", licenseID, o.fieldsForLog())
+
+ if licenseID == "" {
+ return fmt.Errorf("licenseID is empty")
+ }
+
+ if !o.isEditable {
+ fs.Debugf(o.fs, "Object.SetTermsOfUse: permission denied fields={%s}", o.fieldsForLog())
+ return fs.ErrorPermissionDenied
+ }
+
+ err := o.fs.studIPSetTermsOfUse(ctx, o.id, licenseID)
+ if err != nil {
+ fs.Debugf(o.fs, "Object.SetTermsOfUse: failed license=%q fields={%s} err=%v", licenseID, o.fieldsForLog(), err)
+ return err
+ }
+
+ fs.Debugf(o.fs, "Object.SetTermsOfUse: success license=%q fields={%s}", licenseID, o.fieldsForLog())
+
+ return nil
+}
+
+func (o *Object) Remove(ctx context.Context) error {
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+
+ Assert(
+ o != nil,
+ fmt.Sprintf(
+ "o must be not nil; o=%q",
+ o,
+ ),
+ )
+
+ Assert(
+ o.fs != nil,
+ fmt.Sprintf(
+ "o.fs must be not nil; o.fs=%q",
+ o.fs,
+ ),
+ )
+
+ Assert(
+ o.id != "",
+ fmt.Sprintf(
+ "o.id must be not empty; o.id=%q",
+ o.id,
+ ),
+ )
+
+ fs.Debugf(o.fs, "Object.Remove: start fields={%s}", o.fieldsForLog())
+
+ if !o.isEditable && !o.isWritable {
+ fs.Debugf(o.fs, "Object.Remove: permission denied fields={%s}", o.fieldsForLog())
+ return fs.ErrorPermissionDenied
+ }
+
+ err := o.fs.studIPDeleteFile(ctx, o.id)
+ if err != nil {
+ fs.Debugf(o.fs, "Object.Remove: failed fields={%s} err=%v", o.fieldsForLog(), err)
+ return err
+ }
+
+ parent := o.fs.ft.relativeRoot
+ if parent != nil {
+ parentDir := dirPath(o.remote)
+ parent = parent.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)
+ break
+ }
+ }
+ }
+
+ fs.Debugf(o.fs, "Object.Remove: success fields={%s}", o.fieldsForLog())
+
+ return nil
+}