github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/operations/lsjson.go (about)

     1  package operations
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"path"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/rclone/rclone/backend/crypt"
    12  	"github.com/rclone/rclone/fs"
    13  	"github.com/rclone/rclone/fs/hash"
    14  	"github.com/rclone/rclone/fs/walk"
    15  )
    16  
    17  // ListJSONItem in the struct which gets marshalled for each line
    18  type ListJSONItem struct {
    19  	Path          string
    20  	Name          string
    21  	EncryptedPath string `json:",omitempty"`
    22  	Encrypted     string `json:",omitempty"`
    23  	Size          int64
    24  	MimeType      string    `json:",omitempty"`
    25  	ModTime       Timestamp //`json:",omitempty"`
    26  	IsDir         bool
    27  	Hashes        map[string]string `json:",omitempty"`
    28  	ID            string            `json:",omitempty"`
    29  	OrigID        string            `json:",omitempty"`
    30  	Tier          string            `json:",omitempty"`
    31  	IsBucket      bool              `json:",omitempty"`
    32  	Metadata      fs.Metadata       `json:",omitempty"`
    33  }
    34  
    35  // Timestamp a time in the provided format
    36  type Timestamp struct {
    37  	When   time.Time
    38  	Format string
    39  }
    40  
    41  // MarshalJSON turns a Timestamp into JSON
    42  func (t Timestamp) MarshalJSON() (out []byte, err error) {
    43  	if t.When.IsZero() {
    44  		return []byte(`""`), nil
    45  	}
    46  	return []byte(`"` + t.When.Format(t.Format) + `"`), nil
    47  }
    48  
    49  // Returns a time format for the given precision
    50  func formatForPrecision(precision time.Duration) string {
    51  	switch {
    52  	case precision <= time.Nanosecond:
    53  		return "2006-01-02T15:04:05.000000000Z07:00"
    54  	case precision <= 10*time.Nanosecond:
    55  		return "2006-01-02T15:04:05.00000000Z07:00"
    56  	case precision <= 100*time.Nanosecond:
    57  		return "2006-01-02T15:04:05.0000000Z07:00"
    58  	case precision <= time.Microsecond:
    59  		return "2006-01-02T15:04:05.000000Z07:00"
    60  	case precision <= 10*time.Microsecond:
    61  		return "2006-01-02T15:04:05.00000Z07:00"
    62  	case precision <= 100*time.Microsecond:
    63  		return "2006-01-02T15:04:05.0000Z07:00"
    64  	case precision <= time.Millisecond:
    65  		return "2006-01-02T15:04:05.000Z07:00"
    66  	case precision <= 10*time.Millisecond:
    67  		return "2006-01-02T15:04:05.00Z07:00"
    68  	case precision <= 100*time.Millisecond:
    69  		return "2006-01-02T15:04:05.0Z07:00"
    70  	}
    71  	return time.RFC3339
    72  }
    73  
    74  // ListJSONOpt describes the options for ListJSON
    75  type ListJSONOpt struct {
    76  	Recurse       bool     `json:"recurse"`
    77  	NoModTime     bool     `json:"noModTime"`
    78  	NoMimeType    bool     `json:"noMimeType"`
    79  	ShowEncrypted bool     `json:"showEncrypted"`
    80  	ShowOrigIDs   bool     `json:"showOrigIDs"`
    81  	ShowHash      bool     `json:"showHash"`
    82  	DirsOnly      bool     `json:"dirsOnly"`
    83  	FilesOnly     bool     `json:"filesOnly"`
    84  	Metadata      bool     `json:"metadata"`
    85  	HashTypes     []string `json:"hashTypes"` // hash types to show if ShowHash is set, e.g. "MD5", "SHA-1"
    86  }
    87  
    88  // state for ListJson
    89  type listJSON struct {
    90  	fsrc       fs.Fs
    91  	remote     string
    92  	format     string
    93  	opt        *ListJSONOpt
    94  	cipher     *crypt.Cipher
    95  	hashTypes  []hash.Type
    96  	dirs       bool
    97  	files      bool
    98  	canGetTier bool
    99  	isBucket   bool
   100  	showHash   bool
   101  }
   102  
   103  func newListJSON(ctx context.Context, fsrc fs.Fs, remote string, opt *ListJSONOpt) (*listJSON, error) {
   104  	lj := &listJSON{
   105  		fsrc:   fsrc,
   106  		remote: remote,
   107  		opt:    opt,
   108  		dirs:   true,
   109  		files:  true,
   110  	}
   111  	//                       Dirs    Files
   112  	// !FilesOnly,!DirsOnly  true    true
   113  	// !FilesOnly,DirsOnly   true    false
   114  	// FilesOnly,!DirsOnly   false   true
   115  	// FilesOnly,DirsOnly    true    true
   116  	if !opt.FilesOnly && opt.DirsOnly {
   117  		lj.files = false
   118  	} else if opt.FilesOnly && !opt.DirsOnly {
   119  		lj.dirs = false
   120  	}
   121  	if opt.ShowEncrypted {
   122  		fsInfo, _, _, config, err := fs.ConfigFs(fs.ConfigStringFull(fsrc))
   123  		if err != nil {
   124  			return nil, fmt.Errorf("ListJSON failed to load config for crypt remote: %w", err)
   125  		}
   126  		if fsInfo.Name != "crypt" {
   127  			return nil, errors.New("the remote needs to be of type \"crypt\"")
   128  		}
   129  		lj.cipher, err = crypt.NewCipher(config)
   130  		if err != nil {
   131  			return nil, fmt.Errorf("ListJSON failed to make new crypt remote: %w", err)
   132  		}
   133  	}
   134  	features := fsrc.Features()
   135  	lj.canGetTier = features.GetTier
   136  	lj.format = formatForPrecision(fsrc.Precision())
   137  	lj.isBucket = features.BucketBased && remote == "" && fsrc.Root() == "" // if bucket-based remote listing the root mark directories as buckets
   138  	lj.showHash = opt.ShowHash
   139  	lj.hashTypes = fsrc.Hashes().Array()
   140  	if len(opt.HashTypes) != 0 {
   141  		lj.showHash = true
   142  		lj.hashTypes = []hash.Type{}
   143  		for _, hashType := range opt.HashTypes {
   144  			var ht hash.Type
   145  			err := ht.Set(hashType)
   146  			if err != nil {
   147  				return nil, err
   148  			}
   149  			lj.hashTypes = append(lj.hashTypes, ht)
   150  		}
   151  	}
   152  	return lj, nil
   153  }
   154  
   155  // Convert a single entry to JSON
   156  //
   157  // It may return nil if there is no entry to return
   158  func (lj *listJSON) entry(ctx context.Context, entry fs.DirEntry) (*ListJSONItem, error) {
   159  	switch entry.(type) {
   160  	case fs.Directory:
   161  		if lj.opt.FilesOnly {
   162  			return nil, nil
   163  		}
   164  	case fs.Object:
   165  		if lj.opt.DirsOnly {
   166  			return nil, nil
   167  		}
   168  	default:
   169  		fs.Errorf(nil, "Unknown type %T in listing", entry)
   170  	}
   171  
   172  	item := &ListJSONItem{
   173  		Path: entry.Remote(),
   174  		Name: path.Base(entry.Remote()),
   175  		Size: entry.Size(),
   176  	}
   177  	if entry.Remote() == "" {
   178  		item.Name = ""
   179  	}
   180  	if !lj.opt.NoModTime {
   181  		item.ModTime = Timestamp{When: entry.ModTime(ctx), Format: lj.format}
   182  	}
   183  	if !lj.opt.NoMimeType {
   184  		item.MimeType = fs.MimeTypeDirEntry(ctx, entry)
   185  	}
   186  	if lj.cipher != nil {
   187  		switch entry.(type) {
   188  		case fs.Directory:
   189  			item.EncryptedPath = lj.cipher.EncryptDirName(entry.Remote())
   190  		case fs.Object:
   191  			item.EncryptedPath = lj.cipher.EncryptFileName(entry.Remote())
   192  		default:
   193  			fs.Errorf(nil, "Unknown type %T in listing", entry)
   194  		}
   195  		item.Encrypted = path.Base(item.EncryptedPath)
   196  	}
   197  	if lj.opt.Metadata {
   198  		metadata, err := fs.GetMetadata(ctx, entry)
   199  		if err != nil {
   200  			fs.Errorf(entry, "Failed to read metadata: %v", err)
   201  		} else if metadata != nil {
   202  			item.Metadata = metadata
   203  		}
   204  	}
   205  	if do, ok := entry.(fs.IDer); ok {
   206  		item.ID = do.ID()
   207  	}
   208  	if o, ok := entry.(fs.Object); lj.opt.ShowOrigIDs && ok {
   209  		if do, ok := fs.UnWrapObject(o).(fs.IDer); ok {
   210  			item.OrigID = do.ID()
   211  		}
   212  	}
   213  	switch x := entry.(type) {
   214  	case fs.Directory:
   215  		item.IsDir = true
   216  		item.IsBucket = lj.isBucket
   217  	case fs.Object:
   218  		item.IsDir = false
   219  		if lj.showHash {
   220  			item.Hashes = make(map[string]string)
   221  			for _, hashType := range lj.hashTypes {
   222  				hash, err := x.Hash(ctx, hashType)
   223  				if err != nil {
   224  					fs.Errorf(x, "Failed to read hash: %v", err)
   225  				} else if hash != "" {
   226  					item.Hashes[hashType.String()] = hash
   227  				}
   228  			}
   229  		}
   230  		if lj.canGetTier {
   231  			if do, ok := x.(fs.GetTierer); ok {
   232  				item.Tier = do.GetTier()
   233  			}
   234  		}
   235  	default:
   236  		fs.Errorf(nil, "Unknown type %T in listing in ListJSON", entry)
   237  	}
   238  	return item, nil
   239  }
   240  
   241  // ListJSON lists fsrc using the options in opt calling callback for each item
   242  func ListJSON(ctx context.Context, fsrc fs.Fs, remote string, opt *ListJSONOpt, callback func(*ListJSONItem) error) error {
   243  	lj, err := newListJSON(ctx, fsrc, remote, opt)
   244  	if err != nil {
   245  		return err
   246  	}
   247  	err = walk.ListR(ctx, fsrc, remote, false, ConfigMaxDepth(ctx, lj.opt.Recurse), walk.ListAll, func(entries fs.DirEntries) (err error) {
   248  		for _, entry := range entries {
   249  			item, err := lj.entry(ctx, entry)
   250  			if err != nil {
   251  				return fmt.Errorf("creating entry failed in ListJSON: %w", err)
   252  			}
   253  			if item != nil {
   254  				err = callback(item)
   255  				if err != nil {
   256  					return fmt.Errorf("callback failed in ListJSON: %w", err)
   257  				}
   258  			}
   259  		}
   260  		return nil
   261  	})
   262  	if err != nil {
   263  		return fmt.Errorf("error in ListJSON: %w", err)
   264  	}
   265  	return nil
   266  }
   267  
   268  // StatJSON returns a single JSON stat entry for the fsrc, remote path
   269  //
   270  // The item returned may be nil if it is not found or excluded with DirsOnly/FilesOnly
   271  func StatJSON(ctx context.Context, fsrc fs.Fs, remote string, opt *ListJSONOpt) (item *ListJSONItem, err error) {
   272  	// FIXME this could me more efficient we had a new primitive
   273  	// NewDirEntry() which returned an Object or a Directory
   274  	lj, err := newListJSON(ctx, fsrc, remote, opt)
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  
   279  	// Root is always a directory. When we have a NewDirEntry
   280  	// primitive we need to call it, but for now this will do.
   281  	if remote == "" {
   282  		if !lj.dirs {
   283  			return nil, nil
   284  		}
   285  		// Check the root directory exists
   286  		_, err := fsrc.List(ctx, "")
   287  		if err != nil {
   288  			return nil, err
   289  		}
   290  		return lj.entry(ctx, fs.NewDir("", time.Now()))
   291  	}
   292  
   293  	// Could be a file or a directory here
   294  	if lj.files && !strings.HasSuffix(remote, "/") {
   295  		// NewObject can return the sentinel errors ErrorObjectNotFound or ErrorIsDir
   296  		// ErrorObjectNotFound can mean the source is a directory or not found
   297  		obj, err := fsrc.NewObject(ctx, remote)
   298  		if err == fs.ErrorObjectNotFound {
   299  			if !lj.dirs {
   300  				return nil, nil
   301  			}
   302  		} else if err == fs.ErrorIsDir {
   303  			if !lj.dirs {
   304  				return nil, nil
   305  			}
   306  			// This could return a made up ListJSONItem here
   307  			// but that wouldn't have the IDs etc in
   308  		} else if err != nil {
   309  			if !lj.dirs {
   310  				return nil, err
   311  			}
   312  		} else {
   313  			return lj.entry(ctx, obj)
   314  		}
   315  	}
   316  	// Must be a directory here
   317  	//
   318  	// Remove trailing / as rclone listings won't have them
   319  	remote = strings.TrimRight(remote, "/")
   320  	parent := path.Dir(remote)
   321  	if parent == "." || parent == "/" {
   322  		parent = ""
   323  	}
   324  	entries, err := fsrc.List(ctx, parent)
   325  	if err == fs.ErrorDirNotFound {
   326  		return nil, nil
   327  	} else if err != nil {
   328  		return nil, err
   329  	}
   330  	equal := func(a, b string) bool { return a == b }
   331  	if fsrc.Features().CaseInsensitive {
   332  		equal = strings.EqualFold
   333  	}
   334  	var foundEntry fs.DirEntry
   335  	for _, entry := range entries {
   336  		if equal(entry.Remote(), remote) {
   337  			foundEntry = entry
   338  			break
   339  		}
   340  	}
   341  	if foundEntry == nil {
   342  		return nil, nil
   343  	}
   344  	return lj.entry(ctx, foundEntry)
   345  }