From b3a6e33ce1c9677666787c5fb29d1e7dda2c540d Mon Sep 17 00:00:00 2001 From: Xhofe Date: Wed, 16 Feb 2022 20:20:39 +0800 Subject: [PATCH] :sparkler: quark support --- drivers/123/driver.go | 2 +- drivers/all.go | 1 + drivers/baidu/driver.go | 2 +- drivers/mediatrack/driver.go | 2 +- drivers/quark/driver.go | 320 +++++++++++++++++++++++++++++++++++ drivers/quark/quark.go | 266 +++++++++++++++++++++++++++++ drivers/quark/types.go | 134 +++++++++++++++ drivers/quark/util.go | 31 ++++ utils/file.go | 8 + 9 files changed, 763 insertions(+), 3 deletions(-) create mode 100644 drivers/quark/driver.go create mode 100644 drivers/quark/quark.go create mode 100644 drivers/quark/types.go create mode 100644 drivers/quark/util.go diff --git a/drivers/123/driver.go b/drivers/123/driver.go index cc623a82..0491ab36 100644 --- a/drivers/123/driver.go +++ b/drivers/123/driver.go @@ -300,7 +300,7 @@ func (driver Pan123) Upload(file *model.FileStream, account *model.Account) erro return base.ErrNotFolder } parentFileId, _ := strconv.Atoi(parentFile.Id) - tempFile, err := ioutil.TempFile("data/temp", "file-*") + tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*") if err != nil { return err } diff --git a/drivers/all.go b/drivers/all.go index b8d88519..9996382e 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -15,6 +15,7 @@ import ( _ "github.com/Xhofe/alist/drivers/native" _ "github.com/Xhofe/alist/drivers/onedrive" _ "github.com/Xhofe/alist/drivers/pikpak" + _ "github.com/Xhofe/alist/drivers/quark" _ "github.com/Xhofe/alist/drivers/s3" _ "github.com/Xhofe/alist/drivers/shandian" _ "github.com/Xhofe/alist/drivers/teambition" diff --git a/drivers/baidu/driver.go b/drivers/baidu/driver.go index f64c56f8..7493d180 100644 --- a/drivers/baidu/driver.go +++ b/drivers/baidu/driver.go @@ -234,7 +234,7 @@ func (driver Baidu) Upload(file *model.FileStream, account *model.Account) error if file == nil { return base.ErrEmptyFile } - tempFile, err := ioutil.TempFile("data/temp", "file-*") + tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*") if err != nil { return err } diff --git a/drivers/mediatrack/driver.go b/drivers/mediatrack/driver.go index 9e3ab0ab..4d3392bf 100644 --- a/drivers/mediatrack/driver.go +++ b/drivers/mediatrack/driver.go @@ -267,7 +267,7 @@ func (driver MediaTrack) Upload(file *model.FileStream, account *model.Account) if err != nil { return err } - tempFile, err := ioutil.TempFile("data/temp", "file-*") + tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*") if err != nil { return err } diff --git a/drivers/quark/driver.go b/drivers/quark/driver.go new file mode 100644 index 00000000..15eba079 --- /dev/null +++ b/drivers/quark/driver.go @@ -0,0 +1,320 @@ +package quark + +import ( + "crypto/md5" + "crypto/sha1" + "encoding/hex" + "github.com/Xhofe/alist/conf" + "github.com/Xhofe/alist/drivers/base" + "github.com/Xhofe/alist/model" + "github.com/Xhofe/alist/utils" + log "github.com/sirupsen/logrus" + "io" + "io/ioutil" + "os" + "path/filepath" +) + +type Quark struct{} + +func (driver Quark) Config() base.DriverConfig { + return base.DriverConfig{ + Name: "Quark", + OnlyProxy: true, + } +} + +func (driver Quark) Items() []base.Item { + return []base.Item{ + { + Name: "access_token", + Label: "Cookie", + Type: base.TypeString, + Required: true, + Description: "Unknown expiration time", + }, + { + Name: "root_folder", + Label: "root folder file_id", + Type: base.TypeString, + Required: true, + Default: "0", + }, + { + Name: "order_by", + Label: "order_by", + Type: base.TypeSelect, + Values: "file_type,file_name,updated_at", + Required: true, + Default: "file_name", + }, + { + Name: "order_direction", + Label: "order_direction", + Type: base.TypeSelect, + Values: "asc,desc", + Required: true, + Default: "asc", + }, + } +} + +func (driver Quark) Save(account *model.Account, old *model.Account) error { + if account == nil { + return nil + } + _, err := driver.Get("/config", nil, nil, account) + return err +} + +func (driver Quark) File(path string, account *model.Account) (*model.File, error) { + path = utils.ParsePath(path) + if path == "/" { + return &model.File{ + Id: account.RootFolder, + Name: account.Name, + Size: 0, + Type: conf.FOLDER, + Driver: driver.Config().Name, + UpdatedAt: account.UpdatedAt, + }, nil + } + dir, name := filepath.Split(path) + files, err := driver.Files(dir, account) + if err != nil { + return nil, err + } + for _, file := range files { + if file.Name == name { + return &file, nil + } + } + return nil, base.ErrPathNotFound +} + +func (driver Quark) Files(path string, account *model.Account) ([]model.File, error) { + path = utils.ParsePath(path) + var files []model.File + cache, err := base.GetCache(path, account) + if err == nil { + files, _ = cache.([]model.File) + } else { + file, err := driver.File(path, account) + if err != nil { + return nil, err + } + files, err = driver.GetFiles(file.Id, account) + if err != nil { + return nil, err + } + if len(files) > 0 { + _ = base.SetCache(path, files, account) + } + } + return files, nil +} + +func (driver Quark) Link(args base.Args, account *model.Account) (*base.Link, error) { + path := args.Path + file, err := driver.File(path, account) + if err != nil { + return nil, err + } + data := base.Json{ + "fids": []string{file.Id}, + } + var resp DownResp + _, err = driver.Post("/file/download", data, &resp, account) + if err != nil { + return nil, err + } + return &base.Link{ + Url: resp.Data[0].DownloadUrl, + Headers: []base.Header{ + {Name: "Cookie", Value: account.AccessToken}, + }, + }, nil +} + +func (driver Quark) Path(path string, account *model.Account) (*model.File, []model.File, error) { + path = utils.ParsePath(path) + log.Debugf("quark path: %s", path) + file, err := driver.File(path, account) + if err != nil { + return nil, nil, err + } + if !file.IsDir() { + return file, nil, nil + } + files, err := driver.Files(path, account) + if err != nil { + return nil, nil, err + } + return nil, files, nil +} + +func (driver Quark) Preview(path string, account *model.Account) (interface{}, error) { + return nil, base.ErrNotSupport +} + +func (driver Quark) MakeDir(path string, account *model.Account) error { + parentFile, err := driver.File(utils.Dir(path), account) + if err != nil { + return err + } + data := base.Json{ + "dir_init_lock": false, + "dir_path": "", + "file_name": utils.Base(path), + "pdir_fid": parentFile.Id, + } + _, err = driver.Post("/file", data, nil, account) + return err +} + +func (driver Quark) Move(src string, dst string, account *model.Account) error { + srcFile, err := driver.File(src, account) + if err != nil { + return err + } + dstParentFile, err := driver.File(utils.Dir(dst), account) + if err != nil { + return err + } + data := base.Json{ + "action_type": 1, + "exclude_fids": []string{}, + "filelist": []string{srcFile.Id}, + "to_pdir_fid": dstParentFile.Id, + } + _, err = driver.Post("/file/move", data, nil, account) + return err +} + +func (driver Quark) Rename(src string, dst string, account *model.Account) error { + srcFile, err := driver.File(src, account) + if err != nil { + return err + } + data := base.Json{ + "fid": srcFile.Id, + "file_name": utils.Base(dst), + } + _, err = driver.Post("/file/rename", data, nil, account) + return err +} + +func (driver Quark) Copy(src string, dst string, account *model.Account) error { + return base.ErrNotSupport +} + +func (driver Quark) Delete(path string, account *model.Account) error { + srcFile, err := driver.File(path, account) + if err != nil { + return err + } + data := base.Json{ + "action_type": 1, + "exclude_fids": []string{}, + "filelist": []string{srcFile.Id}, + } + _, err = driver.Post("/file/delete", data, nil, account) + return err +} + +func (driver Quark) Upload(file *model.FileStream, account *model.Account) error { + if file == nil { + return base.ErrEmptyFile + } + parentFile, err := driver.File(file.ParentPath, account) + if err != nil { + return err + } + tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*") + if err != nil { + return err + } + defer func() { + _ = tempFile.Close() + _ = os.Remove(tempFile.Name()) + }() + _, err = io.Copy(tempFile, file) + if err != nil { + return err + } + _, err = tempFile.Seek(0, io.SeekStart) + if err != nil { + return err + } + m := md5.New() + _, err = io.Copy(m, tempFile) + if err != nil { + return err + } + _, err = tempFile.Seek(0, io.SeekStart) + if err != nil { + return err + } + md5Str := hex.EncodeToString(m.Sum(nil)) + s := sha1.New() + _, err = io.Copy(s, tempFile) + if err != nil { + return err + } + _, err = tempFile.Seek(0, io.SeekStart) + if err != nil { + return err + } + sha1Str := hex.EncodeToString(s.Sum(nil)) + // pre + pre, err := driver.UpPre(file, parentFile.Id, account) + if err != nil { + return err + } + log.Debugln("hash: ", md5Str, sha1Str) + // hash + finish, err := driver.UpHash(md5Str, sha1Str, pre.Data.TaskId, account) + if err != nil { + return err + } + if finish { + return nil + } + // part up + partSize := pre.Metadata.PartSize + var bytes []byte + md5s := make([]string, 0) + defaultBytes := make([]byte, partSize) + left := int64(file.GetSize()) + partNumber := 1 + for left > 0 { + if left > int64(partSize) { + bytes = defaultBytes + } else { + bytes = make([]byte, left) + } + _, err := io.ReadFull(tempFile, bytes) + if err != nil { + return err + } + left -= int64(partSize) + log.Debugf("left: %d", left) + m, err := driver.UpPart(pre, file.GetMIMEType(), partNumber, bytes, account) + //m, err := driver.UpPart(pre, file.GetMIMEType(), partNumber, bytes, account, md5Str, sha1Str) + if err != nil { + return err + } + if m == "finish" { + return nil + } + md5s = append(md5s, m) + partNumber++ + } + err = driver.UpCommit(pre, md5s, account) + if err != nil { + return err + } + return driver.UpFinish(pre, account) +} + +var _ base.Driver = (*Quark)(nil) diff --git a/drivers/quark/quark.go b/drivers/quark/quark.go new file mode 100644 index 00000000..e193346f --- /dev/null +++ b/drivers/quark/quark.go @@ -0,0 +1,266 @@ +package quark + +import ( + "crypto/md5" + "encoding/base64" + "errors" + "fmt" + "github.com/Xhofe/alist/drivers/base" + "github.com/Xhofe/alist/model" + "github.com/Xhofe/alist/utils" + "github.com/go-resty/resty/v2" + log "github.com/sirupsen/logrus" + "net/http" + "strconv" + "strings" + "time" +) + +func (driver Quark) Request(pathname string, method int, headers, query, form map[string]string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) { + u := "https://drive.quark.cn/1/clouddrive" + pathname + req := base.RestyClient.R() + req.SetHeaders(map[string]string{ + "Cookie": account.AccessToken, + "Accept": "application/json, text/plain, */*", + "Referer": "https://pan.quark.cn/", + }) + req.SetQueryParam("pr", "ucpro") + req.SetQueryParam("fr", "pc") + if headers != nil { + req.SetHeaders(headers) + } + if query != nil { + req.SetQueryParams(query) + } + if form != nil { + req.SetFormData(form) + } + if data != nil { + req.SetBody(data) + } + if resp != nil { + req.SetResult(resp) + } + var e Resp + var err error + var res *resty.Response + req.SetError(&e) + switch method { + case base.Get: + res, err = req.Get(u) + case base.Post: + res, err = req.Post(u) + case base.Delete: + res, err = req.Delete(u) + case base.Patch: + res, err = req.Patch(u) + case base.Put: + res, err = req.Put(u) + default: + return nil, base.ErrNotSupport + } + if err != nil { + return nil, err + } + log.Debugf("%s response: %s", pathname, res.String()) + if e.Status >= 400 || e.Code != 0 { + return nil, errors.New(e.Message) + } + return res.Body(), nil +} + +func (driver Quark) Get(pathname string, query map[string]string, resp interface{}, account *model.Account) ([]byte, error) { + return driver.Request(pathname, base.Get, nil, query, nil, nil, resp, account) +} + +func (driver Quark) Post(pathname string, data interface{}, resp interface{}, account *model.Account) ([]byte, error) { + return driver.Request(pathname, base.Post, nil, nil, nil, data, resp, account) +} + +func (driver Quark) GetFiles(parent string, account *model.Account) ([]model.File, error) { + files := make([]model.File, 0) + page := 1 + size := 100 + query := map[string]string{ + "pdir_fid": parent, + "_size": strconv.Itoa(size), + "_fetch_total": "1", + "_sort": "file_type:asc," + account.OrderBy + ":" + account.OrderDirection, + } + for { + query["_page"] = strconv.Itoa(page) + var resp SortResp + _, err := driver.Get("/file/sort", query, &resp, account) + if err != nil { + return nil, err + } + for _, f := range resp.Data.List { + files = append(files, *driver.formatFile(&f)) + } + if page*size >= resp.Metadata.Count { + break + } + page++ + } + return files, nil +} + +func (driver Quark) UpPre(file *model.FileStream, parentId string, account *model.Account) (UpPreResp, error) { + now := time.Now() + data := base.Json{ + "ccp_hash_update": true, + "dir_name": "", + "file_name": file.Name, + "format_type": file.MIMEType, + "l_created_at": now.UnixMilli(), + "l_updated_at": now.UnixMilli(), + "pdir_fid": parentId, + "size": file.Size, + } + log.Debugf("uppre data: %+v", data) + var resp UpPreResp + _, err := driver.Post("/file/upload/pre", data, &resp, account) + return resp, err +} + +func (driver Quark) UpHash(md5, sha1, taskId string, account *model.Account) (bool, error) { + data := base.Json{ + "md5": md5, + "sha1": sha1, + "task_id": taskId, + } + log.Debugf("hash: %+v", data) + var resp HashResp + _, err := driver.Post("/file/update/hash", data, &resp, account) + return resp.Data.Finish, err +} + +func (driver Quark) UpPart(pre UpPreResp, mineType string, partNumber int, bytes []byte, account *model.Account) (string, error) { + //func (driver Quark) UpPart(pre UpPreResp, mineType string, partNumber int, bytes []byte, account *model.Account, md5Str, sha1Str string) (string, error) { + timeStr := time.Now().UTC().Format(http.TimeFormat) + data := base.Json{ + "auth_info": pre.Data.AuthInfo, + "auth_meta": fmt.Sprintf(`PUT + +%s +%s +x-oss-date:%s +x-oss-user-agent:aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit +/%s/%s?partNumber=%d&uploadId=%s`, + mineType, timeStr, timeStr, pre.Data.Bucket, pre.Data.ObjKey, partNumber, pre.Data.UploadId), + "task_id": pre.Data.TaskId, + } + var resp UpAuthResp + _, err := driver.Post("/file/upload/auth", data, &resp, account) + if err != nil { + return "", err + } + //if partNumber == 1 { + // finish, err := driver.UpHash(md5Str, sha1Str, pre.Data.TaskId, account) + // if err != nil { + // return "", err + // } + // if finish { + // return "finish", nil + // } + //} + u := fmt.Sprintf("https://%s.%s/%s", pre.Data.Bucket, pre.Data.UploadUrl[7:], pre.Data.ObjKey) + res, err := base.RestyClient.R(). + SetHeaders(map[string]string{ + "Authorization": resp.Data.AuthKey, + "Content-Type": mineType, + "Referer": "https://pan.quark.cn/", + "x-oss-date": timeStr, + "x-oss-user-agent": "aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit", + }). + SetQueryParams(map[string]string{ + "partNumber": strconv.Itoa(partNumber), + "uploadId": pre.Data.UploadId, + }).SetBody(bytes).Put(u) + if res.StatusCode() != 200 { + return "", fmt.Errorf("up status: %d, error: %s", res.StatusCode(), res.String()) + } + return res.Header().Get("ETag"), nil +} + +func (driver Quark) UpCommit(pre UpPreResp, md5s []string, account *model.Account) error { + timeStr := time.Now().UTC().Format(http.TimeFormat) + log.Debugf("md5s: %+v", md5s) + bodyBuilder := strings.Builder{} + bodyBuilder.WriteString(` + +`) + for i, m := range md5s { + bodyBuilder.WriteString(fmt.Sprintf(` +%d +%s + +`, i+1, m)) + } + bodyBuilder.WriteString("") + body := bodyBuilder.String() + m := md5.New() + m.Write([]byte(body)) + contentMd5 := base64.StdEncoding.EncodeToString(m.Sum(nil)) + callbackBytes, err := utils.Json.Marshal(pre.Data.Callback) + if err != nil { + return err + } + callbackBase64 := base64.StdEncoding.EncodeToString(callbackBytes) + data := base.Json{ + "auth_info": pre.Data.AuthInfo, + "auth_meta": fmt.Sprintf(`POST +%s +application/xml +%s +x-oss-callback:%s +x-oss-date:%s +x-oss-user-agent:aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit +/%s/%s?uploadId=%s`, + contentMd5, timeStr, callbackBase64, timeStr, + pre.Data.Bucket, pre.Data.ObjKey, pre.Data.UploadId), + "task_id": pre.Data.TaskId, + } + log.Debugf("xml: %s", body) + log.Debugf("auth data: %+v", data) + var resp UpAuthResp + _, err = driver.Post("/file/upload/auth", data, &resp, account) + if err != nil { + return err + } + u := fmt.Sprintf("https://%s.%s/%s", pre.Data.Bucket, pre.Data.UploadUrl[7:], pre.Data.ObjKey) + res, err := base.RestyClient.R(). + SetHeaders(map[string]string{ + "Authorization": resp.Data.AuthKey, + "Content-MD5": contentMd5, + "Content-Type": "application/xml", + "Referer": "https://pan.quark.cn/", + "x-oss-callback": callbackBase64, + "x-oss-date": timeStr, + "x-oss-user-agent": "aliyun-sdk-js/6.6.1 Chrome 98.0.4758.80 on Windows 10 64-bit", + }). + SetQueryParams(map[string]string{ + "uploadId": pre.Data.UploadId, + }).SetBody(body).Post(u) + if res.StatusCode() != 200 { + return fmt.Errorf("up status: %d, error: %s", res.StatusCode(), res.String()) + } + return nil +} + +func (driver Quark) UpFinish(pre UpPreResp, account *model.Account) error { + data := base.Json{ + "obj_key": pre.Data.ObjKey, + "task_id": pre.Data.TaskId, + } + _, err := driver.Post("/file/upload/finish", data, nil, account) + if err != nil { + return err + } + time.Sleep(time.Second) + return nil +} + +func init() { + base.RegisterDriver(&Quark{}) +} diff --git a/drivers/quark/types.go b/drivers/quark/types.go new file mode 100644 index 00000000..ebe77b30 --- /dev/null +++ b/drivers/quark/types.go @@ -0,0 +1,134 @@ +package quark + +type Resp struct { + Status int `json:"status"` + Code int `json:"code"` + Message string `json:"message"` + //ReqId string `json:"req_id"` + //Timestamp int `json:"timestamp"` +} + +type File struct { + Fid string `json:"fid"` + FileName string `json:"file_name"` + //PdirFid string `json:"pdir_fid"` + //Category int `json:"category"` + //FileType int `json:"file_type"` + Size int64 `json:"size"` + //FormatType string `json:"format_type"` + //Status int `json:"status"` + //Tags string `json:"tags,omitempty"` + //LCreatedAt int64 `json:"l_created_at"` + LUpdatedAt int64 `json:"l_updated_at"` + //NameSpace int `json:"name_space"` + //IncludeItems int `json:"include_items,omitempty"` + //RiskType int `json:"risk_type"` + //BackupSign int `json:"backup_sign"` + //Duration int `json:"duration"` + //FileSource string `json:"file_source"` + File bool `json:"file"` + //CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + //PrivateExtra struct {} `json:"_private_extra"` + //ObjCategory string `json:"obj_category,omitempty"` + //Thumbnail string `json:"thumbnail,omitempty"` +} + +type SortResp struct { + Resp + Data struct { + List []File `json:"list"` + } `json:"data"` + Metadata struct { + Size int `json:"_size"` + Page int `json:"_page"` + Count int `json:"_count"` + Total int `json:"_total"` + Way string `json:"way"` + } `json:"metadata"` +} + +type DownResp struct { + Resp + Data []struct { + Fid string `json:"fid"` + FileName string `json:"file_name"` + PdirFid string `json:"pdir_fid"` + Category int `json:"category"` + FileType int `json:"file_type"` + Size int `json:"size"` + FormatType string `json:"format_type"` + Status int `json:"status"` + Tags string `json:"tags"` + LCreatedAt int64 `json:"l_created_at"` + LUpdatedAt int64 `json:"l_updated_at"` + NameSpace int `json:"name_space"` + Thumbnail string `json:"thumbnail"` + DownloadUrl string `json:"download_url"` + Md5 string `json:"md5"` + RiskType int `json:"risk_type"` + RangeSize int `json:"range_size"` + BackupSign int `json:"backup_sign"` + ObjCategory string `json:"obj_category"` + Duration int `json:"duration"` + FileSource string `json:"file_source"` + File bool `json:"file"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + PrivateExtra struct { + } `json:"_private_extra"` + } `json:"data"` + Metadata struct { + Acc2 string `json:"acc2"` + Acc1 string `json:"acc1"` + } `json:"metadata"` +} + +type UpPreResp struct { + Resp + Data struct { + TaskId string `json:"task_id"` + Finish bool `json:"finish"` + UploadId string `json:"upload_id"` + ObjKey string `json:"obj_key"` + UploadUrl string `json:"upload_url"` + Fid string `json:"fid"` + Bucket string `json:"bucket"` + Callback struct { + CallbackUrl string `json:"callbackUrl"` + CallbackBody string `json:"callbackBody"` + } `json:"callback"` + FormatType string `json:"format_type"` + Size int `json:"size"` + AuthInfo string `json:"auth_info"` + } `json:"data"` + Metadata struct { + PartThread int `json:"part_thread"` + Acc2 string `json:"acc2"` + Acc1 string `json:"acc1"` + PartSize int `json:"part_size"` // 分片大小 + } `json:"metadata"` +} + +type HashResp struct { + Resp + Data struct { + Finish bool `json:"finish"` + Fid string `json:"fid"` + Thumbnail string `json:"thumbnail"` + FormatType string `json:"format_type"` + } `json:"data"` + Metadata struct { + } `json:"metadata"` +} + +type UpAuthResp struct { + Resp + Data struct { + AuthKey string `json:"auth_key"` + Speed int `json:"speed"` + Headers []interface{} `json:"headers"` + } `json:"data"` + Metadata struct { + } `json:"metadata"` +} diff --git a/drivers/quark/util.go b/drivers/quark/util.go new file mode 100644 index 00000000..a15e07a7 --- /dev/null +++ b/drivers/quark/util.go @@ -0,0 +1,31 @@ +package quark + +import ( + "github.com/Xhofe/alist/conf" + "github.com/Xhofe/alist/model" + "github.com/Xhofe/alist/utils" + "path" + "time" +) + +func getTime(t int64) *time.Time { + tm := time.UnixMilli(t) + //log.Debugln(tm) + return &tm +} + +func (driver Quark) formatFile(f *File) *model.File { + file := model.File{ + Id: f.Fid, + Name: f.FileName, + Size: f.Size, + Driver: driver.Config().Name, + UpdatedAt: getTime(f.UpdatedAt), + } + if f.File { + file.Type = utils.GetFileType(path.Ext(f.FileName)) + } else { + file.Type = conf.FOLDER + } + return &file +} diff --git a/utils/file.go b/utils/file.go index cfeaa049..ce30e3b4 100644 --- a/utils/file.go +++ b/utils/file.go @@ -127,3 +127,11 @@ func Join(elem ...string) string { func Split(p string) (string, string) { return path.Split(p) } + +// FormatName TODO +func FormatName(name string) string { + name = strings.ReplaceAll(name, "/", " ") + name = strings.ReplaceAll(name, "#", " ") + name = strings.ReplaceAll(name, "?", " ") + return name +}