mirror of
				https://github.com/cloudreve/cloudreve.git
				synced 2025-11-01 00:57:15 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			433 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			433 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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"
 | ||
| 	QuerySearchUseOr          = "use_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[QuerySearchUseOr]; 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)
 | ||
| }
 | ||
| 
 | ||
| func NewMyUri(id string) string {
 | ||
| 	if id == "" {
 | ||
| 		return fmt.Sprintf("%s://%s", constants.CloudreveScheme, constants.FileSystemMy)
 | ||
| 	}
 | ||
| 	return fmt.Sprintf("%s://%s@%s", constants.CloudreveScheme, id, constants.FileSystemMy)
 | ||
| }
 | ||
| 
 | ||
| // 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
 | ||
| }
 | 
