aboutsummaryrefslogtreecommitdiff
path: root/backend/studip/studip.go
diff options
context:
space:
mode:
Diffstat (limited to 'backend/studip/studip.go')
-rw-r--r--backend/studip/studip.go725
1 files changed, 81 insertions, 644 deletions
diff --git a/backend/studip/studip.go b/backend/studip/studip.go
index 46021e2..409f54e 100644
--- a/backend/studip/studip.go
+++ b/backend/studip/studip.go
@@ -1,689 +1,126 @@
package studip
import (
- "context"
- "encoding/json"
- "errors"
"fmt"
- "io"
- "net/http"
- "net/url"
- "os"
- "path"
"path/filepath"
- "slices"
- "sort"
- "strings"
- "time"
+ "runtime"
"github.com/rclone/rclone/fs"
- "github.com/rclone/rclone/fs/config/configmap"
- "github.com/rclone/rclone/fs/config/configstruct"
- "github.com/rclone/rclone/fs/config/obscure"
- "github.com/rclone/rclone/fs/fshttp"
- "github.com/rclone/rclone/fs/hash"
- "github.com/rclone/rclone/lib/rest"
+ "github.com/rclone/rclone/fs/config"
+ "github.com/rclone/rclone/lib/encoder"
)
func init() {
fs.Register(&fs.RegInfo{
Name: "Stud.IP",
- Description: "Stud.IP – read only",
+ Prefix: "studip",
+ Description: "Stud.IP",
NewFs: NewFs,
Options: []fs.Option{{
Name: "base_url",
- Help: "Base URL of Stud.IP installation",
+ Help: "Base URL of the Stud.IP JSON API v1 endpoint",
Default: "https://elearning.uni-bremen.de/jsonapi.php/v1/",
Required: true,
}, {
Name: "username",
- Help: "Stud.IP login name",
+ Help: "Stud.IP username used for login",
Required: true,
}, {
Name: "password",
- Help: "Stud.IP password",
+ Help: "Stud.IP password used for login",
IsPassword: true,
Required: true,
}, {
Name: "course_id",
- Help: "Course ID",
+ Help: "Stud.IP course ID (e.g. 59e88658b39093836455413bd1f24f29)",
Required: true,
+ }, {
+ Name: "license",
+ Help: "License ID applied to uploaded files",
+ Required: true,
+ Default: "UNDEF_LICENSE",
+ Examples: fs.OptionExamples{
+ fs.OptionExample{
+ Value: "FREE_LICENSE",
+ Help: "Works that have been published under a free license, i.e. the distribution and usually also modification of which is permitted without license costs, may be made available for teaching without restrictions. \n\nTypical examples are:\n- Open Access publications \n- Open Educational Resources (OER) \n- Works under Creative Commons licenses (e.g. Wikipedia content) \n\nAttention: Make sure on a case-by-case basis what restrictions on distribution and modification the respective license may contain.",
+ },
+ fs.OptionExample{
+ Value: "SELFMADE_NONPUB",
+ Help: "Self-authored, unpublished work",
+ },
+ fs.OptionExample{
+ Value: "NON_TEXTUAL",
+ Help: "Copyright-protected and published works",
+ },
+ fs.OptionExample{
+ Value: "TEXT_NO_LICENSE",
+ Help: "Published texts without an acquired license or separate permission",
+ },
+ fs.OptionExample{
+ Value: "WITH_LICENSE",
+ Help: "Permission of use or license exists",
+ },
+ fs.OptionExample{
+ Value: "UNDEF_LICENSE",
+ Help: "Unclear License",
+ },
+ },
+ }, {
+ Name: config.ConfigEncoding,
+ Help: config.ConfigEncodingHelp,
+ Advanced: true,
+ Default: (encoder.Base |
+ encoder.EncodeLeftSpace |
+ encoder.EncodeRightSpace |
+ encoder.EncodeCrLf |
+ encoder.EncodeLeftCrLfHtVt |
+ encoder.EncodeRightCrLfHtVt |
+ encoder.EncodeInvalidUtf8),
},
},
})
}
type Options struct {
- BaseURL string `config:"base_url"`
- Username string `config:"username"`
- Password string `config:"password"`
- CourseID string `config:"course_id"`
-}
-
-type Fs struct {
- name string
- opt *Options
- client *rest.Client
- root string
-
- rootNode *Node
-}
-
-type Object struct {
- fs *Fs
- remote string
- id string
- size int64
- isDir bool
- contentType string
- modTime time.Time
-}
-
-func NewFs(
- ctx context.Context,
- name,
- root string,
- m configmap.Mapper,
-) (fs.Fs, error) {
- if ctx.Err() != nil {
- return nil, ctx.Err()
- }
-
- fs.Debugf(name, "initializing studip backend for root %q", root)
- opt := new(Options)
- if err := configstruct.Set(m, opt); err != nil {
- return nil, err
- }
- if opt.CourseID == "" {
- return nil, errors.New("course_id is required")
- }
-
- base, err := url.Parse(opt.BaseURL)
- if err != nil {
- return nil, fmt.Errorf("invalid base_url: %w", err)
- }
-
- var httpClient *rest.Client
- {
- c := fshttp.NewClient(context.Background())
- httpClient = rest.NewClient(c)
- }
-
- httpClient.SetRoot(base.String())
- httpClient.SetHeader("Accept", "application/vnd.api+json")
- httpClient.SetUserPass(opt.Username, obscure.MustReveal(opt.Password))
- httpClient.SetErrorHandler(func(resp *http.Response) error {
- fmt.Println("Status: " + resp.Status)
- fmt.Println("URL: " + resp.Request.URL.String())
- return errors.New("")
- })
-
- f := &Fs{
- name: name,
- opt: opt,
- client: httpClient,
- root: root,
- }
-
- if err := f.TestConnection(ctx); err != nil {
- return nil, err
- }
-
- rootID, err := f.RetrieveRootFolderID(ctx)
- if err != nil {
- return nil, err
- }
-
- rootNode, err := f.GetCourseFileTree(ctx, rootID)
- if err != nil {
- return nil, err
- }
-
- f.rootNode = rootNode
-
- if root != "" {
- pathSplit := splitPath(filepath.Dir(root))
- f.rootNode = GetNodeAtPath(f.rootNode, pathSplit)
- }
-
- return f, nil
-}
-
-type StudIPFolders struct {
- Meta struct {
- Page struct {
- Offset int `json:"offset"`
- Limit int `json:"limit"`
- Total int `json:"total"`
- } `json:"page"`
- } `json:"meta"`
- Data []StudIPFoldersData `json:"data"`
-}
-
-type StudIPFoldersData struct {
- Type string `json:"type"`
- ID string `json:"id"`
- Attributes struct {
- FolderType string `json:"folder-type"`
- Name string `json:"name"`
- Description string `json:"description"`
- Mkdate time.Time `json:"mkdate"`
- Chdate time.Time `json:"chdate"`
- IsVisible bool `json:"is-visible"`
- IsReadable bool `json:"is-readable"`
- IsWritable bool `json:"is-writable"`
- IsEditable bool `json:"is-editable"`
- IsEmpty bool `json:"is-empty"`
- IsSubfolderAllowed bool `json:"is-subfolder-allowed"`
- } `json:"attributes"`
-}
-
-type StudIPFiles struct {
- Meta struct {
- Page struct {
- Offset int `json:"offset"`
- Limit int `json:"limit"`
- Total int `json:"total"`
- } `json:"page"`
- } `json:"meta"`
- Links struct {
- First string `json:"first"`
- Last string `json:"last"`
- } `json:"links"`
- Data []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"`
- } `json:"data"`
-}
-
-type StudIPCourses struct {
- Data struct {
- Type string `json:"type"`
- ID string `json:"id"`
- Attributes struct {
- CourseNumber string `json:"course-number"`
- Title string `json:"title"`
- CourseType int `json:"course-type"`
- CourseTypeText string `json:"course-type-text"`
- Description string `json:"description"`
- Dates string `json:"dates"`
- } `json:"attributes"`
- } `json:"data"`
-}
-
-func (f *Fs) TestConnection(
- ctx context.Context,
-) error {
- if ctx.Err() != nil {
- return ctx.Err()
- }
-
- URL := fmt.Sprintf("courses/%s", f.opt.CourseID)
-
- responseJSON := new(StudIPCourses)
- res, err := f.client.Call(
- ctx,
- &rest.Opts{Method: "GET", Path: URL},
- )
- if err != nil {
- return err
- }
-
- defer res.Body.Close()
- decoder := json.NewDecoder(res.Body)
- err = decoder.Decode(responseJSON)
- if err != nil {
- return err
- }
-
- if responseJSON.Data.ID != f.opt.CourseID {
- return fmt.Errorf("received courseID doesn't match"+
- " configured courseID, received: %s, want: %s",
- responseJSON.Data.ID, f.opt.CourseID)
- }
-
- return nil
-}
-
-func (f *Fs) Name() string { return f.name }
-
-func (f *Fs) Root() string { return f.root }
-func (f *Fs) String() string { return f.opt.BaseURL }
-func (f *Fs) Precision() time.Duration { return time.Second }
-
-func (f *Fs) Hashes() hash.Set { return hash.Set(hash.None) }
-func (f *Fs) Features() *fs.Features {
- return (&fs.Features{CanHaveEmptyDirectories: true}).
- Fill(context.Background(), f)
-}
-
-func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
- return nil, fs.ErrorNotImplemented
-}
-
-// List the objects and directories in dir into entries. The
-// entries can be returned in any order but should be for a
-// complete directory.
-//
-// dir should be "" to list the root, and should not have
-// trailing slashes.
-//
-// This should return ErrDirNotFound if the directory isn't
-// found.
-func (f *Fs) List(
- ctx context.Context,
- dir string,
-) (entries fs.DirEntries, err error) {
- if ctx.Err() != nil {
- return nil, ctx.Err()
- }
-
- pathSplit := strings.Split(dir, string(os.PathSeparator))
-
- node := GetNodeAtPath(f.rootNode, pathSplit)
- if !node.IsDir || node == nil {
- return nil, fs.ErrorDirNotFound
- }
-
- for _, entry := range node.Children {
- if entry.IsDir {
- directory := new(Directory)
- directory.fs = f
- directory.remote = filepath.Join(dir, entry.Name)
- directory.id = entry.Id
- directory.items = int64(len(entry.Children))
- directory.name = entry.Name
- directory.modTime = entry.ChDate
-
- entries = append(entries, directory)
-
- } else {
- object := new(Object)
- object.fs = f
- object.remote = filepath.Join(dir, entry.Name)
- object.id = entry.Id
- object.size = entry.Size
- object.contentType = entry.ContentType
- object.modTime = entry.ChDate
- object.isDir = entry.IsDir
-
- entries = append(entries, object)
- }
- }
-
- sort.Slice(entries, func(i, j int) bool {
- return entries.Less(i, j)
- })
-
- return entries, nil
-}
-
-type Node struct {
- Children []*Node
- Name string
- Id string
- IsDir bool
- ChDate time.Time
- Size int64
- ContentType string
-}
-
-func GetNodeAtPath(
- node *Node,
- pathSplit []string,
-) *Node {
- if len(pathSplit) == 0 {
- return node
- }
-
- if pathSplit[0] == "." {
- return node
- }
-
- if pathSplit[0] == "" {
- return node
- }
-
- for _, children := range node.Children {
- if children.Name == pathSplit[0] {
- return GetNodeAtPath(children, pathSplit[1:])
- }
- }
-
- return nil
-}
-
-func (f *Fs) GetCourseFileTree(
- ctx context.Context,
- rootFolderID string,
-) (*Node, error) {
- if ctx.Err() != nil {
- return nil, ctx.Err()
- }
-
- rootNode := new(Node)
- rootNode.IsDir = true
- rootNode.Id = rootFolderID
-
- err := f.FillFolderNode(ctx, rootNode)
- if err != nil {
- return nil, err
- }
-
- return rootNode, nil
-}
-
-func (f *Fs) FillFolderNode(
- ctx context.Context,
- folderNode *Node,
-) error {
- if ctx.Err() != nil {
- return ctx.Err()
- }
-
- if !folderNode.IsDir {
- return errors.New("node isn't a folder")
- }
-
- folders, err := f.RetrieveFoldersOfFolder(ctx, folderNode.Id)
- if err != nil {
- return err
- }
-
- folderNode.Children = slices.Grow(folderNode.Children, len(folders.Data))
-
- for _, folder := range folders.Data {
- childrenNode := new(Node)
- childrenNode.Id = folder.ID
- childrenNode.IsDir = true
- childrenNode.Name = folder.Attributes.Name
- childrenNode.ChDate = folder.Attributes.Chdate
- childrenNode.Size = -1
-
- folderNode.Children = append(folderNode.Children, childrenNode)
- }
-
- {
- errChan := make(chan error)
- length := len(folderNode.Children)
- {
- for _, childrenNode := range folderNode.Children {
- go func() {
- errChan <- f.FillFolderNode(ctx, childrenNode)
- }()
- }
- }
-
- for range length {
- err := <-errChan
- if err != nil {
- return err
- }
- }
- }
-
- files, err := f.RetrieveFilesOfFolder(ctx, folderNode.Id)
- if err != nil {
- return err
- }
-
- folderNode.Children = slices.Grow(folderNode.Children, len(files.Data))
-
- for _, file := range files.Data {
- if !file.Attributes.IsReadable || !file.Attributes.IsDownloadable {
- continue
- }
-
- childrenNode := new(Node)
- childrenNode.Id = file.ID
- childrenNode.IsDir = false
- childrenNode.Name = file.Attributes.Name
- childrenNode.ChDate = file.Attributes.Chdate
- childrenNode.Size = file.Attributes.Filesize
- childrenNode.ContentType = file.Attributes.MimeType
-
- folderNode.Children = append(folderNode.Children, childrenNode)
- }
-
- return nil
+ BaseURL string `config:"base_url"`
+ Username string `config:"username"`
+ Password string `config:"password"`
+ CourseID string `config:"course_id"`
+ License string `config:"license"`
+ Enc encoder.MultiEncoder `config:"encoding"`
}
-func (f *Fs) RetrieveFoldersOfFolder(
- ctx context.Context,
- folderID string,
-) (*StudIPFolders, error) {
- if ctx.Err() != nil {
- return nil, ctx.Err()
- }
-
- URL := fmt.Sprintf("folders/%s/folders", folderID)
-
- responseJSON := &StudIPFolders{}
- 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) RetrieveFilesOfFolder(
- ctx context.Context,
- folderID string,
-) (*StudIPFiles, error) {
- if ctx.Err() != nil {
- return nil, ctx.Err()
- }
-
- URL := fmt.Sprintf("folders/%s/file-refs", folderID)
-
- responseJSON := &StudIPFiles{}
- 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) RetrieveRootFolderID(
- ctx context.Context,
-) (id string, err error) {
- if ctx.Err() != nil {
- return "", ctx.Err()
- }
-
- URL := fmt.Sprintf("courses/%s/folders", f.opt.CourseID)
-
- responseJSON := &StudIPFolders{}
- res, err := f.client.CallJSON(ctx,
- &rest.Opts{Method: "GET", Path: URL}, nil, responseJSON)
- if err != nil {
- return "", err
- }
- defer res.Body.Close()
-
- index := slices.IndexFunc(responseJSON.Data,
- func(e StudIPFoldersData) bool { return e.Attributes.FolderType == "RootFolder" },
- )
-
- if index == -1 {
- return "", errors.New("response doesn't contain a RootFolder")
- }
-
- return responseJSON.Data[index].ID, nil
-}
-
-func (f *Fs) Put(
- ctx context.Context,
- in io.Reader,
- src fs.ObjectInfo,
- options ...fs.OpenOption,
-) (fs.Object, error) {
- return nil, fs.ErrorPermissionDenied
-}
-func (f *Fs) Mkdir(ctx context.Context, dir string) error { return fs.ErrorPermissionDenied }
-func (f *Fs) Rmdir(ctx context.Context, dir string) error { return fs.ErrorPermissionDenied }
-func (f *Fs) Purge(ctx context.Context, dir string) error {
- return fs.ErrorPermissionDenied
-}
-func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
- return nil, fs.ErrorPermissionDenied
-}
-func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
- return nil, fs.ErrorPermissionDenied
-}
-func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
- return fs.ErrorPermissionDenied
-}
-
-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 { return fs.ErrorNotImplemented }
-func (o *Object) MimeType(ctx context.Context) string { return o.contentType }
+var (
+ _ fs.Fs = &Fs{}
+ _ fs.Object = &Object{}
+ //_ fs.MimeTyper = &Object{}
+ _ fs.Directory = &Directory{}
+)
-func (o *Object) Open(
- ctx context.Context,
- options ...fs.OpenOption,
-) (io.ReadCloser, error) {
- if ctx.Err() != nil {
- return nil, ctx.Err()
+func Assert(cond bool, msg string) {
+ if cond {
+ return
}
- URL := fmt.Sprintf("file-refs/%s/content", o.id)
-
- opts := rest.Opts{Method: "GET", Path: URL}
- var err error
- opts.Options = options
- res, err := o.fs.client.Call(ctx, &opts)
- if err != nil {
- return nil, err
+ if msg == "" {
+ msg = "condition is false"
}
- if res.StatusCode/100 != 2 {
- defer res.Body.Close()
- return nil, fmt.Errorf("HTTP %s", res.Status)
+ pc, file, line, ok := runtime.Caller(1)
+ if !ok {
+ panic("assert failed: " + msg)
}
- return res.Body, 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 {
- return fs.ErrorNotImplemented
-}
-
-func (o *Object) Remove(ctx context.Context) error { return fs.ErrorNotImplemented }
-
-type Directory struct {
- fs *Fs
- id string
- name string
- items int64
- modTime time.Time
- remote string
-}
-
-func (dir *Directory) Fs() fs.Info {
- return dir.fs
-}
-
-func (dir *Directory) ID() string {
- return dir.id
-}
-
-func (dir *Directory) Items() int64 {
- return dir.items
-}
-
-func (dir *Directory) String() string {
- return dir.name
-}
-
-func (dir *Directory) ModTime(context.Context) time.Time {
- return dir.modTime
-}
-
-func (dir *Directory) Remote() string {
- return dir.remote
-}
-
-func (dir *Directory) Size() int64 {
- return -1
-}
-
-// Check the interfaces are satisfied
-var (
- _ fs.Info = &Fs{}
- _ fs.Fs = &Fs{}
- _ fs.Object = &Object{}
- _ fs.MimeTyper = &Object{}
- _ fs.Directory = &Directory{}
-)
-func splitPath(p string) []string {
- p = path.Clean(p)
- if p == "/" {
- return []string{}
+ fnName := "<unknown>"
+ if fn := runtime.FuncForPC(pc); fn != nil {
+ fnName = fn.Name()
}
- return strings.Split(p, "/")
+ panic(fmt.Sprintf(
+ "assert failed at %s:%d (%s): %s",
+ filepath.Base(file),
+ line,
+ fnName,
+ msg,
+ ))
}