sia.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. // Package sia provides an interface to the Sia storage system.
  2. package sia
  3. import (
  4. "context"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "net/http"
  10. "net/url"
  11. "path"
  12. "strings"
  13. "time"
  14. "github.com/rclone/rclone/backend/sia/api"
  15. "github.com/rclone/rclone/fs"
  16. "github.com/rclone/rclone/fs/config"
  17. "github.com/rclone/rclone/fs/config/configmap"
  18. "github.com/rclone/rclone/fs/config/configstruct"
  19. "github.com/rclone/rclone/fs/config/obscure"
  20. "github.com/rclone/rclone/fs/fserrors"
  21. "github.com/rclone/rclone/fs/fshttp"
  22. "github.com/rclone/rclone/fs/hash"
  23. "github.com/rclone/rclone/lib/encoder"
  24. "github.com/rclone/rclone/lib/pacer"
  25. "github.com/rclone/rclone/lib/rest"
  26. )
  27. const (
  28. minSleep = 10 * time.Millisecond
  29. maxSleep = 2 * time.Second
  30. decayConstant = 2 // bigger for slower decay, exponential
  31. )
  32. // Register with Fs
  33. func init() {
  34. fs.Register(&fs.RegInfo{
  35. Name: "sia",
  36. Description: "Sia Decentralized Cloud",
  37. NewFs: NewFs,
  38. Options: []fs.Option{{
  39. Name: "api_url",
  40. Help: `Sia daemon API URL, like http://sia.daemon.host:9980.
  41. Note that siad must run with --disable-api-security to open API port for other hosts (not recommended).
  42. Keep default if Sia daemon runs on localhost.`,
  43. Default: "http://127.0.0.1:9980",
  44. Sensitive: true,
  45. }, {
  46. Name: "api_password",
  47. Help: `Sia Daemon API Password.
  48. Can be found in the apipassword file located in HOME/.sia/ or in the daemon directory.`,
  49. IsPassword: true,
  50. }, {
  51. Name: "user_agent",
  52. Help: `Siad User Agent
  53. Sia daemon requires the 'Sia-Agent' user agent by default for security`,
  54. Default: "Sia-Agent",
  55. Advanced: true,
  56. }, {
  57. Name: config.ConfigEncoding,
  58. Help: config.ConfigEncodingHelp,
  59. Advanced: true,
  60. Default: encoder.EncodeInvalidUtf8 |
  61. encoder.EncodeCtl |
  62. encoder.EncodeDel |
  63. encoder.EncodeHashPercent |
  64. encoder.EncodeQuestion |
  65. encoder.EncodeDot |
  66. encoder.EncodeSlash,
  67. },
  68. }})
  69. }
  70. // Options defines the configuration for this backend
  71. type Options struct {
  72. APIURL string `config:"api_url"`
  73. APIPassword string `config:"api_password"`
  74. UserAgent string `config:"user_agent"`
  75. Enc encoder.MultiEncoder `config:"encoding"`
  76. }
  77. // Fs represents a remote siad
  78. type Fs struct {
  79. name string // name of this remote
  80. root string // the path we are working on if any
  81. opt Options // parsed config options
  82. features *fs.Features // optional features
  83. srv *rest.Client // the connection to siad
  84. pacer *fs.Pacer // pacer for API calls
  85. }
  86. // Object describes a Sia object
  87. type Object struct {
  88. fs *Fs
  89. remote string
  90. modTime time.Time
  91. size int64
  92. }
  93. // Return a string version
  94. func (o *Object) String() string {
  95. if o == nil {
  96. return "<nil>"
  97. }
  98. return o.remote
  99. }
  100. // Remote returns the remote path
  101. func (o *Object) Remote() string {
  102. return o.remote
  103. }
  104. // ModTime is the last modified time (read-only)
  105. func (o *Object) ModTime(ctx context.Context) time.Time {
  106. return o.modTime
  107. }
  108. // Size is the file length
  109. func (o *Object) Size() int64 {
  110. return o.size
  111. }
  112. // Fs returns the parent Fs
  113. func (o *Object) Fs() fs.Info {
  114. return o.fs
  115. }
  116. // Hash is not supported
  117. func (o *Object) Hash(ctx context.Context, ty hash.Type) (string, error) {
  118. return "", hash.ErrUnsupported
  119. }
  120. // Storable returns if this object is storable
  121. func (o *Object) Storable() bool {
  122. return true
  123. }
  124. // SetModTime is not supported
  125. func (o *Object) SetModTime(ctx context.Context, t time.Time) error {
  126. return fs.ErrorCantSetModTime
  127. }
  128. // Open an object for read
  129. func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
  130. var optionsFixed []fs.OpenOption
  131. for _, opt := range options {
  132. if optRange, ok := opt.(*fs.RangeOption); ok {
  133. // Ignore range option if file is empty
  134. if o.Size() == 0 && optRange.Start == 0 && optRange.End > 0 {
  135. continue
  136. }
  137. }
  138. optionsFixed = append(optionsFixed, opt)
  139. }
  140. var resp *http.Response
  141. opts := rest.Opts{
  142. Method: "GET",
  143. Path: path.Join("/renter/stream/", o.fs.root, o.fs.opt.Enc.FromStandardPath(o.remote)),
  144. Options: optionsFixed,
  145. }
  146. err = o.fs.pacer.Call(func() (bool, error) {
  147. resp, err = o.fs.srv.Call(ctx, &opts)
  148. return o.fs.shouldRetry(resp, err)
  149. })
  150. if err != nil {
  151. return nil, err
  152. }
  153. return resp.Body, err
  154. }
  155. // Update the object with the contents of the io.Reader
  156. func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) {
  157. size := src.Size()
  158. var resp *http.Response
  159. opts := rest.Opts{
  160. Method: "POST",
  161. Path: path.Join("/renter/uploadstream/", o.fs.opt.Enc.FromStandardPath(path.Join(o.fs.root, o.remote))),
  162. Body: in,
  163. ContentLength: &size,
  164. Parameters: url.Values{},
  165. }
  166. opts.Parameters.Set("force", "true")
  167. err = o.fs.pacer.Call(func() (bool, error) {
  168. resp, err = o.fs.srv.Call(ctx, &opts)
  169. return o.fs.shouldRetry(resp, err)
  170. })
  171. if err == nil {
  172. err = o.readMetaData(ctx)
  173. }
  174. return err
  175. }
  176. // Remove an object
  177. func (o *Object) Remove(ctx context.Context) (err error) {
  178. var resp *http.Response
  179. opts := rest.Opts{
  180. Method: "POST",
  181. Path: path.Join("/renter/delete/", o.fs.opt.Enc.FromStandardPath(path.Join(o.fs.root, o.remote))),
  182. }
  183. err = o.fs.pacer.Call(func() (bool, error) {
  184. resp, err = o.fs.srv.Call(ctx, &opts)
  185. return o.fs.shouldRetry(resp, err)
  186. })
  187. return err
  188. }
  189. // sync the size and other metadata down for the object
  190. func (o *Object) readMetaData(ctx context.Context) (err error) {
  191. opts := rest.Opts{
  192. Method: "GET",
  193. Path: path.Join("/renter/file/", o.fs.opt.Enc.FromStandardPath(path.Join(o.fs.root, o.remote))),
  194. }
  195. var result api.FileResponse
  196. var resp *http.Response
  197. err = o.fs.pacer.Call(func() (bool, error) {
  198. resp, err = o.fs.srv.CallJSON(ctx, &opts, nil, &result)
  199. return o.fs.shouldRetry(resp, err)
  200. })
  201. if err != nil {
  202. return err
  203. }
  204. o.size = int64(result.File.Filesize)
  205. o.modTime = result.File.ModTime
  206. return nil
  207. }
  208. // Name of the remote (as passed into NewFs)
  209. func (f *Fs) Name() string {
  210. return f.name
  211. }
  212. // Root of the remote (as passed into NewFs)
  213. func (f *Fs) Root() string {
  214. return f.root
  215. }
  216. // String converts this Fs to a string
  217. func (f *Fs) String() string {
  218. return fmt.Sprintf("Sia %s", f.opt.APIURL)
  219. }
  220. // Precision is unsupported because ModTime is not changeable
  221. func (f *Fs) Precision() time.Duration {
  222. return fs.ModTimeNotSupported
  223. }
  224. // Hashes are not exposed anywhere
  225. func (f *Fs) Hashes() hash.Set {
  226. return hash.Set(hash.None)
  227. }
  228. // Features for this fs
  229. func (f *Fs) Features() *fs.Features {
  230. return f.features
  231. }
  232. // List files and directories in a directory
  233. func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
  234. dirPrefix := f.opt.Enc.FromStandardPath(path.Join(f.root, dir)) + "/"
  235. var result api.DirectoriesResponse
  236. var resp *http.Response
  237. opts := rest.Opts{
  238. Method: "GET",
  239. Path: path.Join("/renter/dir/", dirPrefix) + "/",
  240. }
  241. err = f.pacer.Call(func() (bool, error) {
  242. resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
  243. return f.shouldRetry(resp, err)
  244. })
  245. if err != nil {
  246. return nil, err
  247. }
  248. for _, directory := range result.Directories {
  249. if directory.SiaPath+"/" == dirPrefix {
  250. continue
  251. }
  252. d := fs.NewDir(f.opt.Enc.ToStandardPath(strings.TrimPrefix(directory.SiaPath, f.opt.Enc.FromStandardPath(f.root)+"/")), directory.MostRecentModTime)
  253. entries = append(entries, d)
  254. }
  255. for _, file := range result.Files {
  256. o := &Object{fs: f,
  257. remote: f.opt.Enc.ToStandardPath(strings.TrimPrefix(file.SiaPath, f.opt.Enc.FromStandardPath(f.root)+"/")),
  258. modTime: file.ModTime,
  259. size: int64(file.Filesize)}
  260. entries = append(entries, o)
  261. }
  262. return entries, nil
  263. }
  264. // NewObject finds the Object at remote. If it can't be found
  265. // it returns the error fs.ErrorObjectNotFound.
  266. func (f *Fs) NewObject(ctx context.Context, remote string) (o fs.Object, err error) {
  267. obj := &Object{
  268. fs: f,
  269. remote: remote,
  270. }
  271. err = obj.readMetaData(ctx)
  272. if err != nil {
  273. return nil, err
  274. }
  275. return obj, nil
  276. }
  277. // Put the object into the remote siad via uploadstream
  278. func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
  279. o := &Object{
  280. fs: f,
  281. remote: src.Remote(),
  282. modTime: src.ModTime(ctx),
  283. size: src.Size(),
  284. }
  285. err := o.Update(ctx, in, src, options...)
  286. if err == nil {
  287. return o, nil
  288. }
  289. // Cleanup stray files left after failed upload
  290. for i := 0; i < 5; i++ {
  291. cleanObj, cleanErr := f.NewObject(ctx, src.Remote())
  292. if cleanErr == nil {
  293. cleanErr = cleanObj.Remove(ctx)
  294. }
  295. if cleanErr == nil {
  296. break
  297. }
  298. if cleanErr != fs.ErrorObjectNotFound {
  299. fs.Logf(f, "%q: cleanup failed upload: %v", src.Remote(), cleanErr)
  300. break
  301. }
  302. time.Sleep(100 * time.Millisecond)
  303. }
  304. return nil, err
  305. }
  306. // PutStream the object into the remote siad via uploadstream
  307. func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
  308. return f.Put(ctx, in, src, options...)
  309. }
  310. // Mkdir creates a directory
  311. func (f *Fs) Mkdir(ctx context.Context, dir string) (err error) {
  312. var resp *http.Response
  313. opts := rest.Opts{
  314. Method: "POST",
  315. Path: path.Join("/renter/dir/", f.opt.Enc.FromStandardPath(path.Join(f.root, dir))),
  316. Parameters: url.Values{},
  317. }
  318. opts.Parameters.Set("action", "create")
  319. err = f.pacer.Call(func() (bool, error) {
  320. resp, err = f.srv.Call(ctx, &opts)
  321. return f.shouldRetry(resp, err)
  322. })
  323. if err == fs.ErrorDirExists {
  324. err = nil
  325. }
  326. return err
  327. }
  328. // Rmdir removes a directory
  329. func (f *Fs) Rmdir(ctx context.Context, dir string) (err error) {
  330. var resp *http.Response
  331. opts := rest.Opts{
  332. Method: "GET",
  333. Path: path.Join("/renter/dir/", f.opt.Enc.FromStandardPath(path.Join(f.root, dir))),
  334. }
  335. var result api.DirectoriesResponse
  336. err = f.pacer.Call(func() (bool, error) {
  337. resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
  338. return f.shouldRetry(resp, err)
  339. })
  340. if len(result.Directories) == 0 {
  341. return fs.ErrorDirNotFound
  342. } else if len(result.Files) > 0 || len(result.Directories) > 1 {
  343. return fs.ErrorDirectoryNotEmpty
  344. }
  345. opts = rest.Opts{
  346. Method: "POST",
  347. Path: path.Join("/renter/dir/", f.opt.Enc.FromStandardPath(path.Join(f.root, dir))),
  348. Parameters: url.Values{},
  349. }
  350. opts.Parameters.Set("action", "delete")
  351. err = f.pacer.Call(func() (bool, error) {
  352. resp, err = f.srv.Call(ctx, &opts)
  353. return f.shouldRetry(resp, err)
  354. })
  355. return err
  356. }
  357. // NewFs constructs an Fs from the path
  358. func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
  359. // Parse config into Options struct
  360. opt := new(Options)
  361. err := configstruct.Set(m, opt)
  362. if err != nil {
  363. return nil, err
  364. }
  365. opt.APIURL = strings.TrimSuffix(opt.APIURL, "/")
  366. // Parse the endpoint
  367. u, err := url.Parse(opt.APIURL)
  368. if err != nil {
  369. return nil, err
  370. }
  371. rootIsDir := strings.HasSuffix(root, "/")
  372. root = strings.Trim(root, "/")
  373. f := &Fs{
  374. name: name,
  375. opt: *opt,
  376. root: root,
  377. }
  378. f.pacer = fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant)))
  379. f.features = (&fs.Features{
  380. CanHaveEmptyDirectories: true,
  381. }).Fill(ctx, f)
  382. // Adjust client config and pass it attached to context
  383. cliCtx, cliCfg := fs.AddConfig(ctx)
  384. if opt.UserAgent != "" {
  385. cliCfg.UserAgent = opt.UserAgent
  386. }
  387. f.srv = rest.NewClient(fshttp.NewClient(cliCtx))
  388. f.srv.SetRoot(u.String())
  389. f.srv.SetErrorHandler(errorHandler)
  390. if opt.APIPassword != "" {
  391. opt.APIPassword, err = obscure.Reveal(opt.APIPassword)
  392. if err != nil {
  393. return nil, fmt.Errorf("couldn't decrypt API password: %w", err)
  394. }
  395. f.srv.SetUserPass("", opt.APIPassword)
  396. }
  397. if root != "" && !rootIsDir {
  398. // Check to see if the root actually an existing file
  399. remote := path.Base(root)
  400. f.root = path.Dir(root)
  401. if f.root == "." {
  402. f.root = ""
  403. }
  404. _, err := f.NewObject(ctx, remote)
  405. if err != nil {
  406. if errors.Is(err, fs.ErrorObjectNotFound) || errors.Is(err, fs.ErrorNotAFile) {
  407. // File doesn't exist so return old f
  408. f.root = root
  409. return f, nil
  410. }
  411. return nil, err
  412. }
  413. // return an error with an fs which points to the parent
  414. return f, fs.ErrorIsFile
  415. }
  416. return f, nil
  417. }
  418. // errorHandler translates Siad errors into native rclone filesystem errors.
  419. // Sadly this is using string matching since Siad can't expose meaningful codes.
  420. func errorHandler(resp *http.Response) error {
  421. body, err := rest.ReadBody(resp)
  422. if err != nil {
  423. return fmt.Errorf("error when trying to read error body: %w", err)
  424. }
  425. // Decode error response
  426. errResponse := new(api.Error)
  427. err = json.Unmarshal(body, &errResponse)
  428. if err != nil {
  429. // Set the Message to be the body if we can't parse the JSON
  430. errResponse.Message = strings.TrimSpace(string(body))
  431. }
  432. errResponse.Status = resp.Status
  433. errResponse.StatusCode = resp.StatusCode
  434. msg := strings.Trim(errResponse.Message, "[]")
  435. code := errResponse.StatusCode
  436. switch {
  437. case code == 400 && msg == "no file known with that path":
  438. return fs.ErrorObjectNotFound
  439. case code == 400 && strings.HasPrefix(msg, "unable to get the fileinfo from the filesystem") && strings.HasSuffix(msg, "path does not exist"):
  440. return fs.ErrorObjectNotFound
  441. case code == 500 && strings.HasPrefix(msg, "failed to create directory") && strings.HasSuffix(msg, "a siadir already exists at that location"):
  442. return fs.ErrorDirExists
  443. case code == 500 && strings.HasPrefix(msg, "failed to get directory contents") && strings.HasSuffix(msg, "path does not exist"):
  444. return fs.ErrorDirNotFound
  445. case code == 500 && strings.HasSuffix(msg, "no such file or directory"):
  446. return fs.ErrorDirNotFound
  447. }
  448. return errResponse
  449. }
  450. // shouldRetry returns a boolean as to whether this resp and err
  451. // deserve to be retried. It returns the err as a convenience
  452. func (f *Fs) shouldRetry(resp *http.Response, err error) (bool, error) {
  453. return fserrors.ShouldRetry(err), err
  454. }