|
|
package fs
|
|
|
|
|
|
import (
|
|
|
"encoding/json"
|
|
|
"fmt"
|
|
|
"net/url"
|
|
|
"path"
|
|
|
"strconv"
|
|
|
"strings"
|
|
|
"time"
|
|
|
|
|
|
"github.com/cloudreve/Cloudreve/v4/application/constants"
|
|
|
"github.com/cloudreve/Cloudreve/v4/inventory"
|
|
|
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
|
|
"github.com/samber/lo"
|
|
|
)
|
|
|
|
|
|
const (
|
|
|
Separator = "/"
|
|
|
)
|
|
|
|
|
|
const (
|
|
|
QuerySearchName = "name"
|
|
|
QuerySearchNameOpOr = "name_op_or"
|
|
|
QuerySearchMetadataPrefix = "meta_"
|
|
|
QuerySearchCaseFolding = "case_folding"
|
|
|
QuerySearchType = "type"
|
|
|
QuerySearchTypeCategory = "category"
|
|
|
QuerySearchSizeGte = "size_gte"
|
|
|
QuerySearchSizeLte = "size_lte"
|
|
|
QuerySearchCreatedGte = "created_gte"
|
|
|
QuerySearchCreatedLte = "created_lte"
|
|
|
QuerySearchUpdatedGte = "updated_gte"
|
|
|
QuerySearchUpdatedLte = "updated_lte"
|
|
|
)
|
|
|
|
|
|
type URI struct {
|
|
|
U *url.URL
|
|
|
}
|
|
|
|
|
|
func NewUriFromString(u string) (*URI, error) {
|
|
|
raw, err := url.Parse(u)
|
|
|
if err != nil {
|
|
|
return nil, fmt.Errorf("failed to parse uri: %w", err)
|
|
|
}
|
|
|
|
|
|
if raw.Scheme != constants.CloudreveScheme {
|
|
|
return nil, fmt.Errorf("unknown scheme: %s", raw.Scheme)
|
|
|
}
|
|
|
|
|
|
if strings.HasSuffix(raw.Path, Separator) {
|
|
|
raw.Path = strings.TrimSuffix(raw.Path, Separator)
|
|
|
}
|
|
|
|
|
|
return &URI{U: raw}, nil
|
|
|
}
|
|
|
|
|
|
func NewUriFromStrings(u ...string) ([]*URI, error) {
|
|
|
res := make([]*URI, 0, len(u))
|
|
|
for _, uri := range u {
|
|
|
fsUri, err := NewUriFromString(uri)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
res = append(res, fsUri)
|
|
|
}
|
|
|
|
|
|
return res, nil
|
|
|
}
|
|
|
|
|
|
func (u *URI) UnmarshalBinary(text []byte) error {
|
|
|
raw, err := url.Parse(string(text))
|
|
|
if err != nil {
|
|
|
return fmt.Errorf("failed to parse uri: %w", err)
|
|
|
}
|
|
|
|
|
|
u.U = raw
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func (u *URI) MarshalBinary() ([]byte, error) {
|
|
|
return u.U.MarshalBinary()
|
|
|
}
|
|
|
|
|
|
func (u *URI) MarshalJSON() ([]byte, error) {
|
|
|
r := map[string]string{
|
|
|
"uri": u.String(),
|
|
|
}
|
|
|
return json.Marshal(r)
|
|
|
}
|
|
|
|
|
|
func (u *URI) UnmarshalJSON(text []byte) error {
|
|
|
r := make(map[string]string)
|
|
|
err := json.Unmarshal(text, &r)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
u.U, err = url.Parse(r["uri"])
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func (u *URI) String() string {
|
|
|
return u.U.String()
|
|
|
}
|
|
|
|
|
|
func (u *URI) Name() string {
|
|
|
return path.Base(u.Path())
|
|
|
}
|
|
|
|
|
|
func (u *URI) Dir() string {
|
|
|
return path.Dir(u.Path())
|
|
|
}
|
|
|
|
|
|
func (u *URI) Elements() []string {
|
|
|
res := strings.Split(u.PathTrimmed(), Separator)
|
|
|
if len(res) == 1 && res[0] == "" {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
return res
|
|
|
}
|
|
|
|
|
|
func (u *URI) ID(defaultUid string) string {
|
|
|
if u.U.User == nil {
|
|
|
if u.FileSystem() != constants.FileSystemShare {
|
|
|
return defaultUid
|
|
|
}
|
|
|
return ""
|
|
|
}
|
|
|
|
|
|
return u.U.User.Username()
|
|
|
}
|
|
|
|
|
|
func (u *URI) Path() string {
|
|
|
p := u.U.Path
|
|
|
if !strings.HasPrefix(u.U.Path, Separator) {
|
|
|
p = Separator + u.U.Path
|
|
|
}
|
|
|
|
|
|
return path.Clean(p)
|
|
|
}
|
|
|
|
|
|
func (u *URI) PathTrimmed() string {
|
|
|
return strings.TrimPrefix(u.Path(), Separator)
|
|
|
}
|
|
|
|
|
|
func (u *URI) Password() string {
|
|
|
if u.U.User == nil {
|
|
|
return ""
|
|
|
}
|
|
|
|
|
|
pwd, _ := u.U.User.Password()
|
|
|
return pwd
|
|
|
}
|
|
|
|
|
|
func (u *URI) Join(elem ...string) *URI {
|
|
|
newUrl, _ := url.Parse(u.U.String())
|
|
|
return &URI{U: newUrl.JoinPath(lo.Map(elem, func(s string, i int) string {
|
|
|
return PathEscape(s)
|
|
|
})...)}
|
|
|
}
|
|
|
|
|
|
// Join path with raw string
|
|
|
func (u *URI) JoinRaw(elem string) *URI {
|
|
|
return u.Join(strings.Split(strings.TrimPrefix(elem, Separator), Separator)...)
|
|
|
}
|
|
|
|
|
|
func (u *URI) DirUri() *URI {
|
|
|
newUrl, _ := url.Parse(u.U.String())
|
|
|
newUrl.Path = path.Dir(newUrl.Path)
|
|
|
|
|
|
return &URI{U: newUrl}
|
|
|
}
|
|
|
|
|
|
func (u *URI) Root() *URI {
|
|
|
newUrl, _ := url.Parse(u.U.String())
|
|
|
newUrl.Path = Separator
|
|
|
newUrl.RawQuery = ""
|
|
|
|
|
|
return &URI{U: newUrl}
|
|
|
}
|
|
|
|
|
|
func (u *URI) SetQuery(q string) *URI {
|
|
|
newUrl, _ := url.Parse(u.U.String())
|
|
|
newUrl.RawQuery = q
|
|
|
return &URI{U: newUrl}
|
|
|
}
|
|
|
|
|
|
func (u *URI) IsSame(p *URI, uid string) bool {
|
|
|
return p.FileSystem() == u.FileSystem() && p.ID(uid) == u.ID(uid) && u.Path() == p.Path()
|
|
|
}
|
|
|
|
|
|
// Rebased returns a new URI with the path rebased to the given base URI. It is
|
|
|
// commnly used in WebDAV address translation with shared folder symlink.
|
|
|
func (u *URI) Rebase(target, base *URI) *URI {
|
|
|
targetPath := target.Path()
|
|
|
basePath := base.Path()
|
|
|
rebasedPath := strings.TrimPrefix(targetPath, basePath)
|
|
|
|
|
|
newUrl, _ := url.Parse(u.U.String())
|
|
|
newUrl.Path = path.Join(newUrl.Path, rebasedPath)
|
|
|
return &URI{U: newUrl}
|
|
|
}
|
|
|
|
|
|
func (u *URI) FileSystem() constants.FileSystemType {
|
|
|
return constants.FileSystemType(strings.ToLower(u.U.Host))
|
|
|
}
|
|
|
|
|
|
// SearchParameters returns the search parameters from the URI. If no search parameters are present, nil is returned.
|
|
|
func (u *URI) SearchParameters() *inventory.SearchFileParameters {
|
|
|
q := u.U.Query()
|
|
|
res := &inventory.SearchFileParameters{
|
|
|
Metadata: make(map[string]string),
|
|
|
}
|
|
|
withSearch := false
|
|
|
|
|
|
if names, ok := q[QuerySearchName]; ok {
|
|
|
withSearch = len(names) > 0
|
|
|
res.Name = names
|
|
|
}
|
|
|
|
|
|
if _, ok := q[QuerySearchNameOpOr]; ok {
|
|
|
res.NameOperatorOr = true
|
|
|
}
|
|
|
|
|
|
if _, ok := q[QuerySearchCaseFolding]; ok {
|
|
|
res.CaseFolding = true
|
|
|
}
|
|
|
|
|
|
if v, ok := q[QuerySearchTypeCategory]; ok {
|
|
|
res.Category = v[0]
|
|
|
withSearch = withSearch || len(res.Category) > 0
|
|
|
}
|
|
|
|
|
|
if t, ok := q[QuerySearchType]; ok {
|
|
|
fileType := types.FileTypeFromString(t[0])
|
|
|
res.Type = &fileType
|
|
|
withSearch = true
|
|
|
}
|
|
|
|
|
|
for k, v := range q {
|
|
|
if strings.HasPrefix(k, QuerySearchMetadataPrefix) {
|
|
|
res.Metadata[strings.TrimPrefix(k, QuerySearchMetadataPrefix)] = v[0]
|
|
|
withSearch = true
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if v, ok := q[QuerySearchSizeGte]; ok {
|
|
|
limit, err := strconv.ParseInt(v[0], 10, 64)
|
|
|
if err == nil {
|
|
|
res.SizeGte = limit
|
|
|
withSearch = true
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if v, ok := q[QuerySearchSizeLte]; ok {
|
|
|
limit, err := strconv.ParseInt(v[0], 10, 64)
|
|
|
if err == nil {
|
|
|
res.SizeLte = limit
|
|
|
withSearch = true
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if v, ok := q[QuerySearchCreatedGte]; ok {
|
|
|
limit, err := strconv.ParseInt(v[0], 10, 64)
|
|
|
if err == nil {
|
|
|
limit := time.Unix(limit, 0)
|
|
|
res.CreatedAtGte = &limit
|
|
|
withSearch = true
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if v, ok := q[QuerySearchCreatedLte]; ok {
|
|
|
limit, err := strconv.ParseInt(v[0], 10, 64)
|
|
|
if err == nil {
|
|
|
limit := time.Unix(limit, 0)
|
|
|
res.CreatedAtLte = &limit
|
|
|
withSearch = true
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if v, ok := q[QuerySearchUpdatedGte]; ok {
|
|
|
limit, err := strconv.ParseInt(v[0], 10, 64)
|
|
|
if err == nil {
|
|
|
limit := time.Unix(limit, 0)
|
|
|
res.UpdatedAtGte = &limit
|
|
|
withSearch = true
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if v, ok := q[QuerySearchUpdatedLte]; ok {
|
|
|
limit, err := strconv.ParseInt(v[0], 10, 64)
|
|
|
if err == nil {
|
|
|
limit := time.Unix(limit, 0)
|
|
|
res.UpdatedAtLte = &limit
|
|
|
withSearch = true
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if withSearch {
|
|
|
return res
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// EqualOrIsDescendantOf returns true if the URI is equal to the given URI or if it is a descendant of the given URI.
|
|
|
func (u *URI) EqualOrIsDescendantOf(p *URI, uid string) bool {
|
|
|
prefix := p.Path()
|
|
|
if prefix[len(prefix)-1] != Separator[0] {
|
|
|
prefix += Separator
|
|
|
}
|
|
|
|
|
|
return p.FileSystem() == u.FileSystem() && p.ID(uid) == u.ID(uid) &&
|
|
|
(strings.HasPrefix(u.Path(), prefix) || u.Path() == p.Path())
|
|
|
}
|
|
|
|
|
|
func SearchCategoryFromString(s string) setting.SearchCategory {
|
|
|
switch s {
|
|
|
case "image":
|
|
|
return setting.CategoryImage
|
|
|
case "video":
|
|
|
return setting.CategoryVideo
|
|
|
case "audio":
|
|
|
return setting.CategoryAudio
|
|
|
case "document":
|
|
|
return setting.CategoryDocument
|
|
|
default:
|
|
|
return setting.CategoryUnknown
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func NewShareUri(id, password string) string {
|
|
|
if password != "" {
|
|
|
return fmt.Sprintf("%s://%s:%s@%s", constants.CloudreveScheme, id, password, constants.FileSystemShare)
|
|
|
}
|
|
|
return fmt.Sprintf("%s://%s@%s", constants.CloudreveScheme, id, constants.FileSystemShare)
|
|
|
}
|
|
|
|
|
|
// PathEscape is same as url.PathEscape, with modifications to incoporate with JS encodeURIComponent:
|
|
|
// encodeURI() escapes all characters except:
|
|
|
//
|
|
|
// A–Z a–z 0–9 - _ . ! ~ * ' ( )
|
|
|
func PathEscape(s string) string {
|
|
|
hexCount := 0
|
|
|
for i := 0; i < len(s); i++ {
|
|
|
c := s[i]
|
|
|
if shouldEscape(c) {
|
|
|
hexCount++
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if hexCount == 0 {
|
|
|
return s
|
|
|
}
|
|
|
|
|
|
var buf [64]byte
|
|
|
var t []byte
|
|
|
|
|
|
required := len(s) + 2*hexCount
|
|
|
if required <= len(buf) {
|
|
|
t = buf[:required]
|
|
|
} else {
|
|
|
t = make([]byte, required)
|
|
|
}
|
|
|
|
|
|
if hexCount == 0 {
|
|
|
copy(t, s)
|
|
|
for i := 0; i < len(s); i++ {
|
|
|
if s[i] == ' ' {
|
|
|
t[i] = '+'
|
|
|
}
|
|
|
}
|
|
|
return string(t)
|
|
|
}
|
|
|
|
|
|
j := 0
|
|
|
for i := 0; i < len(s); i++ {
|
|
|
switch c := s[i]; {
|
|
|
case shouldEscape(c):
|
|
|
t[j] = '%'
|
|
|
t[j+1] = upperhex[c>>4]
|
|
|
t[j+2] = upperhex[c&15]
|
|
|
j += 3
|
|
|
default:
|
|
|
t[j] = s[i]
|
|
|
j++
|
|
|
}
|
|
|
}
|
|
|
return string(t)
|
|
|
}
|
|
|
|
|
|
const upperhex = "0123456789ABCDEF"
|
|
|
|
|
|
// Return true if the specified character should be escaped when
|
|
|
// appearing in a URL string, according to RFC 3986.
|
|
|
//
|
|
|
// Please be informed that for now shouldEscape does not check all
|
|
|
// reserved characters correctly. See golang.org/issue/5684.
|
|
|
func shouldEscape(c byte) bool {
|
|
|
// §2.3 Unreserved characters (alphanum)
|
|
|
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' {
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
switch c {
|
|
|
case '-', '_', '.', '~', '!', '*', '\'', '(', ')': // §2.3 Unreserved characters (mark)
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
// Everything else must be escaped.
|
|
|
return true
|
|
|
}
|