Conversation
**Added Missing Operations:**
- COPY - Server-side blob copying via WebDAV COPY method
- PROPERTIES - Retrieve blob metadata (ContentLength, ETag, LastModified)
- ENSURE-STORAGE-EXISTS - Initialize WebDAV directory structure
- SIGN - Generate pre-signed URLs with HMAC-SHA256
- DELETE-RECURSIVE - Delete all blobs matching a prefix
**Structural Changes:**
- Split into two-layer architecture like other providers (S3, Azure, etc.)
- client.go: High-level DavBlobstore implementing storage.Storager interface
- storage_client.go: Low-level StorageClient handling HTTP/WebDAV operations
19fa8b5 to
a69e251
Compare
a69e251 to
93b80d1
Compare
e6899c3 to
230cd5a
Compare
230cd5a to
cc66878
Compare
4e7c568 to
9a45943
Compare
…ect names, not basenames.Change low-level Exists to (bool, error), matching Azure/GCS/S3.
Extend WebDAV signer to support both BOSH-compatible SHA256 HMAC signing and CAPI-compatible MD5 signing via optional signing_method config field. - Add SigningMethod field to config (default: "sha256") - Implement generateSHA256SignedURL() for BOSH (/signed/ paths) - Implement generateMD5SignedURL() for CAPI (/read/, /write/ paths) - Add HEAD verb support - Improve WebDAV compatibility with PROPFIND - Simplify blob path handling for CCNG pre-partitioned IDs - Add comprehensive tests for both signing methods
1. Simple blob IDs (like my-blob) get stored with hash-prefix directory structure (8c/my-blob) 2. Pre-partitioned blob IDs (like ru/by/ruby-buildpack from CCNG) are stored as-is 3. Both PUT and COPY operations correctly create parent directories by using the built path (with hash prefix) when calling ensureObjectParentsExist
Preserve endpoint base paths when constructing signed URLs to ensure
directory keys (cc-droplets, cc-packages, etc.) are included in the
final URL path. The signer now combines:
- Path prefix (/signed/, /read/, or /write/)
- Endpoint base path (directory key from config)
- Blob ID with hash prefix
This ensures storage-cli and fog/webdav clients store blobs at
identical physical locations for backward compatibility.
Also fixed DAV integration tests to validate signed URL generation
without requiring nginx infrastructure in the test environment.
dav/client/storage_client.go
Outdated
|
|
||
| // Build the full blob path (with CCNG partitioning if using signed URLs) | ||
| useSignedURLs := c.config.Secret != "" | ||
| blobPath := buildBlobPath(path, useSignedURLs) |
There was a problem hiding this comment.
Not sure that storage-cli should invent a CC specific blob path. I would expect the CC to provide an already correct path, i.e. call storage-cli -s dav -c dav-config.json put blob.file /12/34/1234-...
The other storage types don't manipulate the blob path either.
There was a problem hiding this comment.
Good point — I removed the CC-specific blob path rewriting from the DAV client, it no longer applies any partitioning or prefixing on its own.
Make DAV client consistent with S3/Azure/GCS/AliOSS by removing path transformation logic. Storage-cli now uses blob paths exactly as provided by the caller. Cloud Controller handles partitioning via BlobKeyGenerator before calling storage-cli. - Remove buildBlobPath() and path partitioning - Simplify List() to generic WebDAV traversal - Remove "webdav" → "dav" alias (CC handles this) - Fix Sign() and Copy() consistency issues - Update README to reflect caller-defined paths
dav/signer/signer_test.go
Outdated
| // timestamp: 1566817860 (2019-08-26 11:11:00 UTC) | ||
| // expires: 1566818760 (timestamp + 900 seconds = 15 minutes later) | ||
| // Signature matches BOSH secure_link_hmac format: $request_method$object_id$arg_ts$arg_e | ||
| expected := "https://api.example.com/signed/fake-object-id?e=1566818760&st=YUBIL21YRsFY_w-NrYiAPUnIhlenFuLEa6WsQUhpGLI&ts=1566817860" |
There was a problem hiding this comment.
Why does the expected signature link change? SHA256 signatures (Bosh format) should not change.
dav/signer/signer.go
Outdated
| expiresAfterSeconds := int(expiresAfter.Seconds()) | ||
| signature := s.generateSignature(prefixedBlobID, verb, timeStamp, expiresAfterSeconds) | ||
| timestamp := timeStamp.Unix() | ||
| expires := timestamp + int64(expiresAfter.Seconds()) |
There was a problem hiding this comment.
If I got it right: old implementation uses expiresAfterSeconds for the signature, the new implementation an absolute timestamp. This might be more correct for an expiration, but is it compatible with Bosh?
There was a problem hiding this comment.
Oh, you're right - I accidentally changed the BOSH signature format. Will revert it!
dav/client/storage_client.go
Outdated
|
|
||
| if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { | ||
| err := fmt.Errorf("invalid status: %d", resp.StatusCode) | ||
| return fmt.Errorf("deleting blob %q: %w", path, err) |
There was a problem hiding this comment.
Could be merged into one fmt.Errorf().
dav/client/storage_client.go
Outdated
| } | ||
|
|
||
| // newPropfindBody creates a properly formatted PROPFIND request body | ||
| func newPropfindBody() (*strings.Reader, error) { |
There was a problem hiding this comment.
Looks like this function returns a static string (no parameters).
newPropfindBody() is called e.g. in listRecursive() -> there is no need to create the xml again and again.
There was a problem hiding this comment.
True. I changed it to generate the PROPFIND XML once at initialization time and now just create a new reader from the static string for each request.
dav/client/client_test.go
Outdated
| err := client.Exists("/somefile") | ||
| Expect(err).NotTo(HaveOccurred()) | ||
| // We can't easily test this without refactoring to inject storageClient | ||
| // This is a structural example |
There was a problem hiding this comment.
Is this test case really testing anything? The FakeStorageClient is not used by DavBlobstore.
There was a problem hiding this comment.
You're right — the previous test was not actually using the fake storage client through DavBlobstore.
I changed the tests to inject the fake via NewWithStorageClient, so they now verify that DavBlobstore correctly delegates to the underlying StorageClient.
Added Missing Operations:
Structural Changes: