Skip to content

Refactor DAV client, add missing operations#70

Open
kathap wants to merge 22 commits intomainfrom
add-missing-operations-for-webdav
Open

Refactor DAV client, add missing operations#70
kathap wants to merge 22 commits intomainfrom
add-missing-operations-for-webdav

Conversation

@kathap
Copy link
Copy Markdown
Contributor

@kathap kathap commented Mar 13, 2026

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

 **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
@kathap kathap force-pushed the add-missing-operations-for-webdav branch 2 times, most recently from 19fa8b5 to a69e251 Compare March 13, 2026 15:54
@kathap kathap force-pushed the add-missing-operations-for-webdav branch from a69e251 to 93b80d1 Compare March 13, 2026 15:57
@github-project-automation github-project-automation bot moved this from Inbox to Waiting for Changes | Open for Contribution in Foundational Infrastructure Working Group Mar 13, 2026
@kathap kathap force-pushed the add-missing-operations-for-webdav branch 3 times, most recently from e6899c3 to 230cd5a Compare March 13, 2026 16:14
@kathap kathap force-pushed the add-missing-operations-for-webdav branch from 230cd5a to cc66878 Compare March 13, 2026 16:17
@kathap kathap force-pushed the add-missing-operations-for-webdav branch from 4e7c568 to 9a45943 Compare March 16, 2026 15:13
kathap added 5 commits March 25, 2026 10:47
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.

// Build the full blob path (with CCNG partitioning if using signed URLs)
useSignedURLs := c.config.Secret != ""
blobPath := buildBlobPath(path, useSignedURLs)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

kathap added 2 commits April 10, 2026 12:06
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
// 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"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the expected signature link change? SHA256 signatures (Bosh format) should not change.

expiresAfterSeconds := int(expiresAfter.Seconds())
signature := s.generateSignature(prefixedBlobID, verb, timeStamp, expiresAfterSeconds)
timestamp := timeStamp.Unix()
expires := timestamp + int64(expiresAfter.Seconds())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, you're right - I accidentally changed the BOSH signature format. Will revert it!


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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be merged into one fmt.Errorf().

}

// newPropfindBody creates a properly formatted PROPFIND request body
func newPropfindBody() (*strings.Reader, error) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

err := client.Exists("/somefile")
Expect(err).NotTo(HaveOccurred())
// We can't easily test this without refactoring to inject storageClient
// This is a structural example
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this test case really testing anything? The FakeStorageClient is not used by DavBlobstore.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Waiting for Changes | Open for Contribution

Development

Successfully merging this pull request may close these issues.

3 participants