Repository: rclone/rclone
Pull request #6680 (2023-01-09 12:42:12)

storj: implement public link
<!--
Thank you very much for contributing code or documentation to rclone! Please
fill out the following questions to make it easier for us to review your
changes.

You do not need to check all the boxes below all at once, feel free to take
your time and add more commits. If you're done and ready for review, please
check the last box.
-->

#### What is the purpose of this change?

<!--
Describe the changes here
-->

Implement the generation of a public link for the Storj backend.

#### Was the change discussed in an issue or in the forum before?

<!--
Link issues and relevant forum posts here.
-->

No.

#### Checklist

- [x] I have read the [contribution guidelines](https://github.com/rclone/rclone/blob/master/CONTRIBUTING.md#submitting-a-new-feature-or-bug-fix).
- [x] I have added tests for all changes in this PR if appropriate.
- [x] I have added documentation for the changes if appropriate.
- [x] All commit messages are in [house style](https://github.com/rclone/rclone/blob/master/CONTRIBUTING.md#commit-messages).
- [x] I'm done, this Pull Request is ready for review :-)


kaloyan-raev: Output of the integration tests against the production Storj network:
```
=== RUN   TestIntegration
    fstests.go:421: Using remote "TestStorj:"
=== RUN   TestIntegration/FsCheckWrap
    fstests.go:462: Not a wrapping Fs
=== RUN   TestIntegration/FsCommand
    fstests.go:490: No commands in this remote
=== RUN   TestIntegration/FsRmdirNotFound
=== RUN   TestIntegration/FsString
=== RUN   TestIntegration/FsName
=== RUN   TestIntegration/FsRoot
=== RUN   TestIntegration/FsRmdirEmpty
=== RUN   TestIntegration/FsMkdir
=== RUN   TestIntegration/FsMkdir/FsMkdirRmdirSubdir
    fstest.go:250: Filtering empty directory "dir"
    fstest.go:250: Filtering empty directory "dir/subdir"
    fstest.go:250: Filtering empty directory "dir"
=== RUN   TestIntegration/FsMkdir/FsListEmpty
=== RUN   TestIntegration/FsMkdir/FsListDirEmpty
=== RUN   TestIntegration/FsMkdir/FsListRDirEmpty
=== RUN   TestIntegration/FsMkdir/FsListDirNotFound
=== RUN   TestIntegration/FsMkdir/FsListRDirNotFound
=== RUN   TestIntegration/FsMkdir/FsEncoding
=== RUN   TestIntegration/FsMkdir/FsEncoding/control_chars
    fstests.go:677: testing "␀␁␂␃␄␅␆␇␈␉␊␋␌␍␎␏␐␑␒␓␔␕␖␗␘␙␚␛␜␝␞␟␡"
=== RUN   TestIntegration/FsMkdir/FsEncoding/dot
    fstests.go:677: testing "．"
=== RUN   TestIntegration/FsMkdir/FsEncoding/dot_dot
    fstests.go:677: testing "．．"
=== RUN   TestIntegration/FsMkdir/FsEncoding/punctuation
    fstests.go:677: testing "!\"#$%&'()*+,-.／:;<=>?@[\\]^_`{|}~"
=== RUN   TestIntegration/FsMkdir/FsEncoding/leading_space
    fstests.go:677: testing " leading space"
=== RUN   TestIntegration/FsMkdir/FsEncoding/leading_tilde
    fstests.go:677: testing "~leading tilde"
=== RUN   TestIntegration/FsMkdir/FsEncoding/leading_CR
    fstests.go:677: testing "␍leading CR"
=== RUN   TestIntegration/FsMkdir/FsEncoding/leading_LF
    fstests.go:677: testing "␊leading LF"
=== RUN   TestIntegration/FsMkdir/FsEncoding/leading_HT
    fstests.go:677: testing "␉leading HT"
=== RUN   TestIntegration/FsMkdir/FsEncoding/leading_VT
    fstests.go:677: testing "␋leading VT"
=== RUN   TestIntegration/FsMkdir/FsEncoding/leading_dot
    fstests.go:677: testing ".leading dot"
=== RUN   TestIntegration/FsMkdir/FsEncoding/trailing_space
    fstests.go:677: testing "trailing space "
=== RUN   TestIntegration/FsMkdir/FsEncoding/trailing_CR
    fstests.go:677: testing "trailing CR␍"
=== RUN   TestIntegration/FsMkdir/FsEncoding/trailing_LF
    fstests.go:677: testing "trailing LF␊"
=== RUN   TestIntegration/FsMkdir/FsEncoding/trailing_HT
    fstests.go:677: testing "trailing HT␉"
=== RUN   TestIntegration/FsMkdir/FsEncoding/trailing_VT
    fstests.go:677: testing "trailing VT␋"
=== RUN   TestIntegration/FsMkdir/FsEncoding/trailing_dot
    fstests.go:677: testing "trailing dot."
=== RUN   TestIntegration/FsMkdir/FsEncoding/invalid_UTF-8
    fstests.go:677: testing "invalid utf-8\xfe"
=== RUN   TestIntegration/FsMkdir/FsEncoding/URL_encoding
    fstests.go:677: testing "test%46.txt"
=== RUN   TestIntegration/FsMkdir/FsNewObjectNotFound
=== RUN   TestIntegration/FsMkdir/FsPutError
2023/01/09 14:47:14 ERROR : FS sj://rclone-test-gubufel3rijanor6xatunov3: cp input ./hello? sausage/êé/Hello, 世界/ " ' @ < > & ? + ≠/z.txt []: potato
=== RUN   TestIntegration/FsMkdir/FsPutZeroLength
=== RUN   TestIntegration/FsMkdir/FsOpenWriterAt
    fstests.go:750: FS has no OpenWriterAt interface
=== RUN   TestIntegration/FsMkdir/FsChangeNotify
    fstests.go:786: FS has no ChangeNotify interface
=== RUN   TestIntegration/FsMkdir/FsPutFiles
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsListDirFile2
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsListRDirFile2
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsListR
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsListRSubdir
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsListDirRoot
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsListRDirRoot
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsListSubdir
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsListRSubdir#01
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsListLevel2
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsListRLevel2
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsListFile1
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsNewObject
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsNewObjectCaseInsensitive
    fstests.go:1039: Not Case Insensitive
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsListFile1and2
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsNewObjectDir
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsPurge
    fstests.go:1071: FS has no Purge interface
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsCopy
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsMove
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsDirMove
    fstests.go:1213: FS has no DirMover interface
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsRmdirFull
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsPrecision
=== RUN   TestIntegration/FsMkdir/FsPutFiles/ObjectString
=== RUN   TestIntegration/FsMkdir/FsPutFiles/ObjectFs
=== RUN   TestIntegration/FsMkdir/FsPutFiles/ObjectRemote
=== RUN   TestIntegration/FsMkdir/FsPutFiles/ObjectHashes
=== RUN   TestIntegration/FsMkdir/FsPutFiles/ObjectModTime
=== RUN   TestIntegration/FsMkdir/FsPutFiles/ObjectMimeType
    fstests.go:1346: MimeType method not supported
=== RUN   TestIntegration/FsMkdir/FsPutFiles/ObjectMetadata
    fstests.go:1375: Metadata method not supported
=== RUN   TestIntegration/FsMkdir/FsPutFiles/ObjectSetModTime
    fstests.go:1442: can't set modified time
=== RUN   TestIntegration/FsMkdir/FsPutFiles/ObjectSize
=== RUN   TestIntegration/FsMkdir/FsPutFiles/ObjectOpen
=== RUN   TestIntegration/FsMkdir/FsPutFiles/ObjectOpenSeek
=== RUN   TestIntegration/FsMkdir/FsPutFiles/ObjectOpenRange
=== RUN   TestIntegration/FsMkdir/FsPutFiles/ObjectPartialRead
=== RUN   TestIntegration/FsMkdir/FsPutFiles/ObjectUpdate
=== RUN   TestIntegration/FsMkdir/FsPutFiles/ObjectStorable
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsIsFile
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsIsFileNotFound
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FromRoot
    fstests.go:1579: Opening root remote "TestStorj:" path "rclone-test-gubufel3rijanor6xatunov3" from "TestStorj:rclone-test-gubufel3rijanor6xatunov3"
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FromRoot/List
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FromRoot/ListEntries
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FromRoot/ListR
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FromRoot/Put
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FromRoot/Put/Remove
=== RUN   TestIntegration/FsMkdir/FsPutFiles/PublicLink
=== RUN   TestIntegration/FsMkdir/FsPutFiles/SetTier
    fstests.go:395: FS has no SetTier & GetTier interfaces
=== RUN   TestIntegration/FsMkdir/FsPutFiles/ObjectCheckWrap
    fstests.go:1780: Not a wrapping Fs
=== RUN   TestIntegration/FsMkdir/FsPutFiles/ObjectRemove
=== RUN   TestIntegration/FsMkdir/FsPutFiles/ObjectAbout
    fstests.go:1814: FS does not support About
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsPutStream
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsPutStream/0
=== RUN   TestIntegration/FsMkdir/FsPutFiles/FsPutStream/100
=== RUN   TestIntegration/FsMkdir/FsPutFiles/Internal
    fstests.go:1876: *storj.Fs does not implement InternalTester
=== RUN   TestIntegration/FsMkdir/FsPutChunked
    fstests.go:1893: *storj.Fs does not implement SetUploadChunkSizer
=== RUN   TestIntegration/FsMkdir/FsUploadUnknownSize
=== RUN   TestIntegration/FsMkdir/FsUploadUnknownSize/FsPutUnknownSize
=== RUN   TestIntegration/FsMkdir/FsUploadUnknownSize/FsUpdateUnknownSize
=== RUN   TestIntegration/FsMkdir/FsRootCollapse
2023/01/09 14:47:44 ERROR : FS sj://rclone-test-gubufel3rijanor6xatunov3: Failed to list "": directory not found
2023/01/09 14:47:45 ERROR : : Failed to rmdir: directory not found
=== RUN   TestIntegration/FsShutdown
    fstests.go:2089: Shutdown method not supported
--- PASS: TestIntegration (65.59s)
    --- SKIP: TestIntegration/FsCheckWrap (0.00s)
    --- SKIP: TestIntegration/FsCommand (0.00s)
    --- PASS: TestIntegration/FsRmdirNotFound (0.43s)
    --- PASS: TestIntegration/FsString (0.00s)
    --- PASS: TestIntegration/FsName (0.00s)
    --- PASS: TestIntegration/FsRoot (0.00s)
    --- PASS: TestIntegration/FsRmdirEmpty (0.15s)
    --- PASS: TestIntegration/FsMkdir (64.57s)
        --- PASS: TestIntegration/FsMkdir/FsMkdirRmdirSubdir (1.19s)
        --- PASS: TestIntegration/FsMkdir/FsListEmpty (0.14s)
        --- PASS: TestIntegration/FsMkdir/FsListDirEmpty (0.14s)
        --- PASS: TestIntegration/FsMkdir/FsListRDirEmpty (0.14s)
        --- PASS: TestIntegration/FsMkdir/FsListDirNotFound (0.25s)
        --- PASS: TestIntegration/FsMkdir/FsListRDirNotFound (0.14s)
        --- PASS: TestIntegration/FsMkdir/FsEncoding (30.90s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/control_chars (1.58s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/dot (1.58s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/dot_dot (1.58s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/punctuation (1.71s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/leading_space (1.64s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/leading_tilde (1.63s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/leading_CR (1.71s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/leading_LF (1.64s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/leading_HT (1.60s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/leading_VT (1.58s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/leading_dot (1.59s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/trailing_space (1.58s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/trailing_CR (1.58s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/trailing_LF (1.58s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/trailing_HT (1.58s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/trailing_VT (1.59s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/trailing_dot (1.59s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/invalid_UTF-8 (1.72s)
            --- PASS: TestIntegration/FsMkdir/FsEncoding/URL_encoding (1.73s)
        --- PASS: TestIntegration/FsMkdir/FsNewObjectNotFound (0.27s)
        --- PASS: TestIntegration/FsMkdir/FsPutError (0.80s)
        --- PASS: TestIntegration/FsMkdir/FsPutZeroLength (1.18s)
        --- SKIP: TestIntegration/FsMkdir/FsOpenWriterAt (0.00s)
        --- SKIP: TestIntegration/FsMkdir/FsChangeNotify (0.00s)
        --- PASS: TestIntegration/FsMkdir/FsPutFiles (24.81s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsListDirFile2 (0.68s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsListRDirFile2 (0.69s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsListR (0.14s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsListRSubdir (0.14s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsListDirRoot (0.48s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsListRDirRoot (0.38s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsListSubdir (0.27s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsListRSubdir#01 (0.27s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsListLevel2 (0.27s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsListRLevel2 (0.13s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsListFile1 (0.14s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsNewObject (0.13s)
            --- SKIP: TestIntegration/FsMkdir/FsPutFiles/FsNewObjectCaseInsensitive (0.00s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsListFile1and2 (0.14s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsNewObjectDir (0.13s)
            --- SKIP: TestIntegration/FsMkdir/FsPutFiles/FsPurge (0.00s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsCopy (0.95s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsMove (3.32s)
            --- SKIP: TestIntegration/FsMkdir/FsPutFiles/FsDirMove (0.00s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsRmdirFull (0.14s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsPrecision (0.00s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/ObjectString (0.13s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/ObjectFs (0.14s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/ObjectRemote (0.13s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/ObjectHashes (0.13s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/ObjectModTime (0.14s)
            --- SKIP: TestIntegration/FsMkdir/FsPutFiles/ObjectMimeType (0.13s)
            --- SKIP: TestIntegration/FsMkdir/FsPutFiles/ObjectMetadata (0.13s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/ObjectSetModTime (0.13s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/ObjectSize (0.14s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/ObjectOpen (0.32s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/ObjectOpenSeek (0.32s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/ObjectOpenRange (0.88s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/ObjectPartialRead (0.32s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/ObjectUpdate (0.98s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/ObjectStorable (0.13s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsIsFile (0.73s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsIsFileNotFound (0.69s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FromRoot (2.96s)
                --- PASS: TestIntegration/FsMkdir/FsPutFiles/FromRoot/List (0.66s)
                --- PASS: TestIntegration/FsMkdir/FsPutFiles/FromRoot/ListEntries (0.24s)
                --- PASS: TestIntegration/FsMkdir/FsPutFiles/FromRoot/ListR (0.42s)
                --- PASS: TestIntegration/FsMkdir/FsPutFiles/FromRoot/Put (1.64s)
                    --- PASS: TestIntegration/FsMkdir/FsPutFiles/FromRoot/Put/Remove (0.57s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/PublicLink (3.83s)
            --- SKIP: TestIntegration/FsMkdir/FsPutFiles/SetTier (0.00s)
            --- SKIP: TestIntegration/FsMkdir/FsPutFiles/ObjectCheckWrap (0.00s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/ObjectRemove (0.48s)
            --- SKIP: TestIntegration/FsMkdir/FsPutFiles/ObjectAbout (0.00s)
            --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsPutStream (1.60s)
                --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsPutStream/0 (0.81s)
                --- PASS: TestIntegration/FsMkdir/FsPutFiles/FsPutStream/100 (0.79s)
            --- SKIP: TestIntegration/FsMkdir/FsPutFiles/Internal (0.00s)
        --- SKIP: TestIntegration/FsMkdir/FsPutChunked (0.00s)
        --- PASS: TestIntegration/FsMkdir/FsUploadUnknownSize (2.24s)
            --- PASS: TestIntegration/FsMkdir/FsUploadUnknownSize/FsPutUnknownSize (0.66s)
            --- PASS: TestIntegration/FsMkdir/FsUploadUnknownSize/FsUpdateUnknownSize (1.58s)
        --- PASS: TestIntegration/FsMkdir/FsRootCollapse (0.70s)
    --- SKIP: TestIntegration/FsShutdown (0.00s)
PASS
ok  	github.com/rclone/rclone/backend/storj	65.603s
```

Patch:
diff --git a/backend/storj/fs.go b/backend/storj/fs.go
index 9a0a5ebe2..ed3004099 100644
--- a/backend/storj/fs.go
+++ b/backend/storj/fs.go
@@ -23,6 +23,7 @@ import (
 	"golang.org/x/text/unicode/norm"
 
 	"storj.io/uplink"
+	"storj.io/uplink/edge"
 )
 
 const (
@@ -156,11 +157,12 @@ type Fs struct {
 
 // Check the interfaces are satisfied.
 var (
-	_ fs.Fs          = &Fs{}
-	_ fs.ListRer     = &Fs{}
-	_ fs.PutStreamer = &Fs{}
-	_ fs.Mover       = &Fs{}
-	_ fs.Copier      = &Fs{}
+	_ fs.Fs           = &Fs{}
+	_ fs.ListRer      = &Fs{}
+	_ fs.PutStreamer  = &Fs{}
+	_ fs.Mover        = &Fs{}
+	_ fs.Copier       = &Fs{}
+	_ fs.PublicLinker = &Fs{}
 )
 
 // NewFs creates a filesystem backed by Storj.
@@ -545,7 +547,7 @@ func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options .
 	defer func() {
 		if err != nil {
 			aerr := upload.Abort()
-			if aerr != nil {
+			if aerr != nil && !errors.Is(aerr, uplink.ErrUploadDone) {
 				fs.Errorf(f, "cp input ./%s %+v: %+v", src.Remote(), options, aerr)
 			}
 		}
@@ -560,6 +562,16 @@ func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options .
 
 	_, err = io.Copy(upload, in)
 	if err != nil {
+		if errors.Is(err, uplink.ErrBucketNotFound) {
+			// Rclone assumes the backend will create the bucket if not existing yet.
+			// Here we create the bucket and return a retry error for rclone to retry the upload.
+			_, err = f.project.EnsureBucket(ctx, bucketName)
+			if err != nil {
+				return nil, err
+			}
+			return nil, fserrors.RetryError(errors.New("bucket was not available, now created, the upload must be retried"))
+		}
+
 		err = fserrors.RetryError(err)
 		fs.Errorf(f, "cp input ./%s %+v: %+v\n", src.Remote(), options, err)
 
@@ -761,3 +773,55 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
 	// Return the new object
 	return newObjectFromUplink(f, remote, newObject), nil
 }
+
+// PublicLink generates a public link to the remote path (usually readable by anyone)
+func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (string, error) {
+	bucket, key := f.absolute(remote)
+	if len(bucket) == 0 {
+		return "", errors.New("path must be specified")
+	}
+
+	// Rclone requires that a link is only generated if the remote path exists
+	if len(key) == 0 {
+		_, err := f.project.StatBucket(ctx, bucket)
+		if err != nil {
+			return "", err
+		}
+	} else {
+		_, err := f.project.StatObject(ctx, bucket, key)
+		if err != nil {
+			if !errors.Is(err, uplink.ErrObjectNotFound) {
+				return "", err
+			}
+			// No object found, check if there is such a prefix
+			iter := f.project.ListObjects(ctx, bucket, &uplink.ListObjectsOptions{Prefix: key + "/"})
+			if iter.Err() != nil {
+				return "", iter.Err()
+			}
+			if !iter.Next() {
+				return "", err
+			}
+		}
+	}
+
+	sharedPrefix := uplink.SharePrefix{Bucket: bucket, Prefix: key}
+
+	permission := uplink.ReadOnlyPermission()
+	if expire.IsSet() {
+		permission.NotAfter = time.Now().Add(time.Duration(expire))
+	}
+
+	sharedAccess, err := f.access.Share(permission, sharedPrefix)
+	if err != nil {
+		return "", fmt.Errorf("sharing access to object failed: %w", err)
+	}
+
+	creds, err := (&edge.Config{
+		AuthServiceAddress: "auth.storjshare.io:7777",
+	}).RegisterAccess(ctx, sharedAccess, &edge.RegisterAccessOptions{Public: true})
+	if err != nil {
+		return "", fmt.Errorf("creating public link failed: %w", err)
+	}
+
+	return edge.JoinShareURL("https://link.storjshare.io", creds.AccessKeyID, bucket, key, nil)
+}
diff --git a/docs/content/overview.md b/docs/content/overview.md
index eca35d8d4..df19d05a9 100644
--- a/docs/content/overview.md
+++ b/docs/content/overview.md
@@ -504,7 +504,7 @@ upon backend-specific capabilities.
 | Sia                          | No    | No   | No   | No      | No      | No    | Yes          | No           | No    | Yes      |
 | SMB                          | No    | No   | Yes  | Yes     | No      | No    | Yes          | No           | No    | Yes      |
 | SugarSync                    | Yes   | Yes  | Yes  | Yes     | No      | No    | Yes          | Yes          | No    | Yes      |
-| Storj                        | Yes † | Yes  | Yes  | No      | No      | Yes   | Yes          | No           | No    | No       |
+| Storj                        | Yes † | Yes  | Yes  | No      | No      | Yes   | Yes          | Yes          | No    | No       |
 | Uptobox                      | No    | Yes  | Yes  | Yes     | No      | No    | No           | No           | No    | No       |
 | WebDAV                       | Yes   | Yes  | Yes  | Yes     | No      | No    | Yes ‡        | No           | Yes   | Yes      |
 | Yandex Disk                  | Yes   | Yes  | Yes  | Yes     | Yes     | No    | Yes          | Yes          | Yes   | Yes      |


Test patch:
diff --git a/fstest/fstests/fstests.go b/fstest/fstests/fstests.go
index 40fb39851..9c3ea61ff 100644
--- a/fstest/fstests/fstests.go
+++ b/fstest/fstests/fstests.go
@@ -1734,8 +1734,10 @@ func Run(t *testing.T, opt *Opt) {
 					// ensure sub remote isn't empty
 					buf := bytes.NewBufferString("somecontent")
 					obji := object.NewStaticObjectInfo("somefile", time.Now(), int64(buf.Len()), true, nil, nil)
-					_, err = subRemote.Put(ctx, buf, obji)
-					require.NoError(t, err)
+					retry(t, "Put", func() error {
+						_, err := subRemote.Put(ctx, buf, obji)
+						return err
+					})
 
 					link4, err := wrapPublicLinkFunc(subRemote.Features().PublicLink)(ctx, "", expiry, false)
 					require.NoError(t, err, "Sharing root in a sub-remote should work")