Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions drivers/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import (
_ "github.com/OpenListTeam/OpenList/v4/drivers/mediatrack"
_ "github.com/OpenListTeam/OpenList/v4/drivers/mega"
_ "github.com/OpenListTeam/OpenList/v4/drivers/misskey"
_ "github.com/OpenListTeam/OpenList/v4/drivers/modelscope"
_ "github.com/OpenListTeam/OpenList/v4/drivers/mopan"
_ "github.com/OpenListTeam/OpenList/v4/drivers/netease_music"
_ "github.com/OpenListTeam/OpenList/v4/drivers/onedrive"
Expand Down
230 changes: 230 additions & 0 deletions drivers/modelscope/driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package modelscope

import (
"context"
"errors"
"path"
"strings"

"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/errs"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
)

type ModelScope struct {
model.Storage
Addition
}

func (d *ModelScope) Config() driver.Config {
return config
}

func (d *ModelScope) GetAddition() driver.Additional {
return &d.Addition
}

func (d *ModelScope) Init(ctx context.Context) error {
if strings.TrimSpace(d.RepoID) == "" {
return errors.New("repo_id is required")
}
if strings.TrimSpace(d.Endpoint) == "" {
d.Endpoint = "https://modelscope.cn"
}
if strings.TrimSpace(d.Revision) == "" {
d.Revision = "master"
}
if d.RepoType == "" {
d.RepoType = "dataset"
}
if strings.TrimSpace(d.CommitMessage) == "" {
d.CommitMessage = "Upload from OpenList"
}
d.RootFolderPath = utils.FixAndCleanPath(d.RootFolderPath)
return nil
}

func (d *ModelScope) Drop(ctx context.Context) error {
return nil
}

// repoPath maps a storage obj path into a repo-relative path.
//
// The OpenList fs layer already prefixes RootFolderPath onto obj paths (the
// driver's GetRoot returns Path = RootFolderPath), so obj.GetPath() is already
// the repo-relative path. We only normalise it.
func (d *ModelScope) repoPath(objPath string) string {
return normalizePath(objPath)
}

func (d *ModelScope) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
repoDir := d.repoPath(dir.GetPath())

// A directory listing only needs the direct children of repoDir. ModelScope
// returns the full recursive tree, so we fetch it (scoped to repoDir where
// the server supports it) and collapse to direct children.
entries, err := d.getFileList(ctx, repoDir, true)
if err != nil {
return nil, err
}

prefix := repoDir
if prefix != "" {
prefix = utils.PathAddSeparatorSuffix(prefix)
}

seenDirs := map[string]bool{}
objs := make([]model.Obj, 0, len(entries))
for _, e := range entries {
ep := normalizePath(e.getPath())
// Keep only entries directly inside repoDir.
if prefix != "" {
if !strings.HasPrefix(ep, prefix) {
continue
}
}
rel := strings.TrimPrefix(ep, prefix)
if rel == "" {
continue
}
// Skip deeper nesting: only take the first path segment.
seg := rel
if i := strings.Index(rel, "/"); i >= 0 {
seg = rel[:i]
}

name := seg
isDir := e.isDir() || strings.Contains(rel, "/")
if isDir {
if seenDirs[name] {
continue
}
seenDirs[name] = true
}

full := joinRepoPath(repoDir, name)
// Hide our placeholder for empty directories.
if name == ".gitkeep" {
continue
}
objs = append(objs, &model.Object{
Name: name,
Size: e.getSize(),
Modified: toTime(e.modified()),
IsFolder: isDir,
Path: full,
ID: full,
})
}
return objs, nil
}

func (d *ModelScope) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
repoFile := d.repoPath(file.GetPath())
return &model.Link{
URL: d.downloadURL(repoFile),
Header: d.downloadHeaders(),
}, nil
}

// getRootPath returns the storage root object for this driver.
func (d *ModelScope) GetRoot(ctx context.Context) (model.Obj, error) {
return &model.Object{
Name: "root",
Path: d.RootFolderPath,
ID: d.RootFolderPath,
IsFolder: true,
}, nil
}

func (d *ModelScope) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
// ModelScope (git-style) repositories cannot store empty directories, so we
// create a .gitkeep placeholder file.
dirPath := joinRepoPath(d.repoPath(parentDir.GetPath()), dirName)
keepPath := joinRepoPath(dirPath, ".gitkeep")
if err := d.putSmallFile(ctx, keepPath, nil); err != nil {
return nil, err
}
return &model.Object{
Name: dirName,
Path: dirPath,
ID: dirPath,
IsFolder: true,
}, nil
}

func (d *ModelScope) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
filePath := joinRepoPath(d.repoPath(dstDir.GetPath()), stream.GetName())
obj, err := d.uploadFile(ctx, filePath, stream, up)
if err != nil {
return nil, err
}
return obj, nil
}

func (d *ModelScope) Remove(ctx context.Context, obj model.Obj) error {
repoPath := d.repoPath(obj.GetPath())
if obj.IsDir() {
// Removing a directory means removing every file under it.
entries, err := d.getFileList(ctx, repoPath, true)
if err != nil {
return err
}
prefix := repoPath
if prefix != "" {
prefix = utils.PathAddSeparatorSuffix(prefix)
}
var paths []string
for _, e := range entries {
ep := normalizePath(e.getPath())
if ep == repoPath {
continue
}
if repoPath == "" || strings.HasPrefix(ep, prefix) {
if !e.isDir() {
paths = append(paths, ep)
}
}
}
if len(paths) == 0 {
return nil
}
return d.deleteFiles(ctx, paths)
}
return d.deleteFiles(ctx, []string{repoPath})
}

func (d *ModelScope) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
src := d.repoPath(srcObj.GetPath())
dst := joinRepoPath(path.Dir(src), newName)
return d.movePath(ctx, src, dst, srcObj.IsDir())
}

func (d *ModelScope) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
src := d.repoPath(srcObj.GetPath())
dst := joinRepoPath(d.repoPath(dstDir.GetPath()), srcObj.GetName())
if dst == src {
return srcObj, nil
}
if strings.HasPrefix(dst, utils.PathAddSeparatorSuffix(src)) {
return nil, errors.New("cannot move a directory into itself")
}
return d.movePath(ctx, src, dst, srcObj.IsDir())
}

func (d *ModelScope) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
src := d.repoPath(srcObj.GetPath())
dst := joinRepoPath(d.repoPath(dstDir.GetPath()), srcObj.GetName())
if srcObj.IsDir() {
return nil, errs.NotSupport
}
// Copy a single file by downloading and re-uploading.
link, err := d.Link(ctx, srcObj, model.LinkArgs{})
if err != nil {
return nil, err
}
return d.copyFileFromURL(ctx, src, dst, link)
}

var _ driver.Driver = (*ModelScope)(nil)
40 changes: 40 additions & 0 deletions drivers/modelscope/meta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package modelscope

import (
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/op"
)

// Addition holds the user-configurable fields of a ModelScope storage.
//
// A single ModelScope repository (a "repo file driver") maps to one OpenList
// storage mount. ModelScope stores files inside a git-style repository, so the
// natural mapping is:
//
// ModelScope RepoID + RepoType + Revision -> one OpenList storage
//
// See drivers/github for a repository-oriented driver with the same shape.
type Addition struct {
driver.RootPath

Endpoint string `json:"endpoint" type:"select" options:"https://modelscope.cn,https://modelscope.ai" default:"https://modelscope.cn"`
Token string `json:"token" type:"string" required:"true" help:"ModelScope access token from https://modelscope.cn/my/access/token"`
RepoID string `json:"repo_id" type:"string" required:"true" help:"owner/repository, e.g. van/my-files"`
RepoType string `json:"repo_type" type:"select" options:"model,dataset" default:"dataset"`
Revision string `json:"revision" type:"string" default:"master" help:"Branch, tag or commit SHA"`
CommitMessage string `json:"commit_message" type:"string" default:"Upload from OpenList"`
}

var config = driver.Config{
Name: "ModelScope",
LocalSort: true,
DefaultRoot: "/",
CheckStatus: false,
NoOverwriteUpload: false,
}

func init() {
op.RegisterDriver(func() driver.Driver {
return &ModelScope{}
})
}
Loading