diff --git a/docs/_advanced-topics/hooks.md b/docs/_advanced-topics/hooks.md index 968b0e5d9..060fe04f5 100644 --- a/docs/_advanced-topics/hooks.md +++ b/docs/_advanced-topics/hooks.md @@ -83,9 +83,11 @@ Below you can find an annotated, JSON-ish encoded example of a hook request: // Storage contains information about where the upload is stored. The exact values // depend on the storage that is used and are not available in the pre-create hook. "Storage": { - // For example, the filestore supplies the absolute file path: + // For example, the filestore supplies the absolute file paths where the upload data + // (Path) and the associated info file (InfoPath) are stored: "Type": "filestore", "Path": "/my/upload/directory/14b1c4c77771671a8479bc0444bbc5ce", + "InfoPath": "/my/upload/directory/14b1c4c77771671a8479bc0444bbc5ce.info", // The S3Store and GCSStore supply the bucket name and object key: "Type": "s3store", diff --git a/pkg/filestore/filestore.go b/pkg/filestore/filestore.go index 6f9ab5229..b6bb3f004 100644 --- a/pkg/filestore/filestore.go +++ b/pkg/filestore/filestore.go @@ -62,14 +62,22 @@ func (store FileStore) NewUpload(ctx context.Context, info handler.FileInfo) (ha // The binary file's location might be modified by the pre-create hook. var binPath string if info.Storage != nil && info.Storage["Path"] != "" { - binPath = filepath.Join(store.Path, info.Storage["Path"]) + // filepath.Join treats absolute and relative paths the same, so we must + // handle them on our own. Absolute paths get used as-is, while relative + // paths are joined to the storage path. + if filepath.IsAbs(info.Storage["Path"]) { + binPath = info.Storage["Path"] + } else { + binPath = filepath.Join(store.Path, info.Storage["Path"]) + } } else { binPath = store.defaultBinPath(info.ID) } info.Storage = map[string]string{ - "Type": "filestore", - "Path": binPath, + "Type": "filestore", + "Path": binPath, + "InfoPath": infoPath, } // Create binary file with no content diff --git a/pkg/filestore/filestore_test.go b/pkg/filestore/filestore_test.go index d25ed3a3a..4933bdea5 100644 --- a/pkg/filestore/filestore_test.go +++ b/pkg/filestore/filestore_test.go @@ -43,9 +43,10 @@ func TestFilestore(t *testing.T) { a.EqualValues(42, info.Size) a.EqualValues(0, info.Offset) a.Equal(handler.MetaData{"hello": "world"}, info.MetaData) - a.Equal(2, len(info.Storage)) + a.Equal(3, len(info.Storage)) a.Equal("filestore", info.Storage["Type"]) a.Equal(filepath.Join(tmp, info.ID), info.Storage["Path"]) + a.Equal(filepath.Join(tmp, info.ID+".info"), info.Storage["InfoPath"]) // Write data to upload bytesWritten, err := upload.WriteChunk(ctx, 0, strings.NewReader("hello world")) @@ -104,9 +105,10 @@ func TestCreateDirectories(t *testing.T) { a.EqualValues(42, info.Size) a.EqualValues(0, info.Offset) a.Equal(handler.MetaData{"hello": "world"}, info.MetaData) - a.Equal(2, len(info.Storage)) + a.Equal(3, len(info.Storage)) a.Equal("filestore", info.Storage["Type"]) a.Equal(filepath.Join(tmp, info.ID), info.Storage["Path"]) + a.Equal(filepath.Join(tmp, info.ID+".info"), info.Storage["InfoPath"]) // Write data to upload bytesWritten, err := upload.WriteChunk(ctx, 0, strings.NewReader("hello world")) @@ -246,8 +248,9 @@ func TestDeclareLength(t *testing.T) { a.Equal(false, updatedInfo.SizeIsDeferred) } -// TestCustomPath tests whether the upload's destination can be customized. -func TestCustomPath(t *testing.T) { +// TestCustomRelativePath tests whether the upload's destination can be customized +// relative to the storage directory. +func TestCustomRelativePath(t *testing.T) { a := assert.New(t) tmp, err := os.MkdirTemp("", "tusd-filestore-") @@ -272,9 +275,10 @@ func TestCustomPath(t *testing.T) { a.NoError(err) a.EqualValues(42, info.Size) a.EqualValues(0, info.Offset) - a.Equal(2, len(info.Storage)) + a.Equal(3, len(info.Storage)) a.Equal("filestore", info.Storage["Type"]) a.Equal(filepath.Join(tmp, "./folder2/bin"), info.Storage["Path"]) + a.Equal(filepath.Join(tmp, "./folder1/info.info"), info.Storage["InfoPath"]) // Write data to upload bytesWritten, err := upload.WriteChunk(ctx, 0, strings.NewReader("hello world")) @@ -313,3 +317,44 @@ func TestCustomPath(t *testing.T) { a.Equal(nil, upload) a.Equal(handler.ErrNotFound, err) } + +// TestCustomAbsolutePath tests whether the upload's destination can be customized +// using an absolute path to the storage directory. +func TestCustomAbsolutePath(t *testing.T) { + a := assert.New(t) + + tmp1, err := os.MkdirTemp("", "tusd-filestore-") + a.NoError(err) + + tmp2, err := os.MkdirTemp("", "tusd-filestore-") + a.NoError(err) + + store := FileStore{tmp1} + ctx := context.Background() + + // Create new upload, but the Path property points to a directory + // outside of the directory given to FileStore + binPath := filepath.Join(tmp2, "dir/my-upload.bin") + upload, err := store.NewUpload(ctx, handler.FileInfo{ + ID: "my-upload", + Size: 42, + Storage: map[string]string{ + "Path": binPath, + }, + }) + a.NoError(err) + a.NotEqual(nil, upload) + + info, err := upload.GetInfo(ctx) + a.NoError(err) + a.EqualValues(42, info.Size) + a.EqualValues(0, info.Offset) + a.Equal(3, len(info.Storage)) + a.Equal("filestore", info.Storage["Type"]) + a.Equal(binPath, info.Storage["Path"]) + a.Equal(filepath.Join(tmp1, "my-upload.info"), info.Storage["InfoPath"]) + + statInfo, err := os.Stat(binPath) + a.NoError(err) + a.True(statInfo.Mode().IsRegular()) +}