Skip to content

Commit f93f8a2

Browse files
authored
Implement Copy, List, DeleteRecursive, Properties for DAV (#103)
* Implement Copy, List, DeleteRecursive, Properties for DAV Replaces the stubs left from the two-layer refactor (#98). Adds PROPFIND and COPY support via WebDAV verbs and Basic Auth. EnsureStorageExists is a no-op, matching the existing Ruby DavClient — WebDAV has no bucket concept, the blobstore VM provisions the root directory, and nginx auto-creates per-resource subdirs on first PUT. * eliminate buildBlobURL and setAuth duplication * Reject 207 as error and replace DeleteRecursive loop with single DELETE * Start List PROPFIND at prefix path and warn on skipped href * move input validation to client.go, remove from storage_client.go * omit content_length from Properties output when not provided by server * align DeleteRecursive with S3/Azure: list-then-delete, allow empty prefix * always PROPFIND from root in List, warn when DeleteRecursive finds nothing * remove dead code, fix linter * shorten comments
1 parent 89f2ad7 commit f93f8a2

5 files changed

Lines changed: 962 additions & 16 deletions

File tree

dav/client/client.go

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ func NewWithStorageClient(storageClient StorageClient) *DavBlobstore {
5252
func (d *DavBlobstore) Put(sourceFilePath string, dest string) error {
5353
slog.Info("uploading file to webdav", "source", sourceFilePath, "dest", dest)
5454

55+
if err := validateBlobID(dest); err != nil {
56+
return err
57+
}
58+
5559
source, err := os.Open(sourceFilePath)
5660
if err != nil {
5761
return fmt.Errorf("failed to open source file: %w", err)
@@ -75,6 +79,10 @@ func (d *DavBlobstore) Put(sourceFilePath string, dest string) error {
7579
func (d *DavBlobstore) Get(source string, dest string) error {
7680
slog.Info("downloading file from webdav", "source", source, "dest", dest)
7781

82+
if err := validateBlobID(source); err != nil {
83+
return err
84+
}
85+
7886
destFile, err := os.Create(dest)
7987
if err != nil {
8088
return fmt.Errorf("failed to create destination file: %w", err)
@@ -98,17 +106,25 @@ func (d *DavBlobstore) Get(source string, dest string) error {
98106

99107
func (d *DavBlobstore) Delete(dest string) error {
100108
slog.Info("deleting file from webdav", "dest", dest)
109+
if err := validateBlobID(dest); err != nil {
110+
return err
111+
}
101112
return d.storageClient.Delete(dest)
102113
}
103114

104115
func (d *DavBlobstore) Exists(dest string) (bool, error) {
105116
slog.Info("checking if file exists on webdav", "dest", dest)
117+
if err := validateBlobID(dest); err != nil {
118+
return false, err
119+
}
106120
return d.storageClient.Exists(dest)
107121
}
108122

109123
func (d *DavBlobstore) Sign(dest string, action string, expiration time.Duration) (string, error) {
110124
slog.Info("signing url for webdav", "dest", dest, "action", action, "expiration", expiration)
111-
125+
if err := validateBlobID(dest); err != nil {
126+
return "", err
127+
}
112128
action = strings.ToUpper(action)
113129
switch action {
114130
case "GET", "PUT":
@@ -122,27 +138,41 @@ func (d *DavBlobstore) Sign(dest string, action string, expiration time.Duration
122138
}
123139
}
124140

125-
// DeleteRecursive is not yet implemented in this refactoring
126141
func (d *DavBlobstore) DeleteRecursive(prefix string) error {
127-
return fmt.Errorf("DeleteRecursive not yet implemented")
142+
slog.Info("deleting blobs recursively from webdav", "prefix", prefix)
143+
return d.storageClient.DeleteRecursive(prefix)
128144
}
129145

130-
// List is not yet implemented in this refactoring
131146
func (d *DavBlobstore) List(prefix string) ([]string, error) {
132-
return nil, fmt.Errorf("List not yet implemented")
147+
slog.Info("listing blobs on webdav", "prefix", prefix)
148+
if prefix != "" {
149+
if err := validatePrefix(prefix); err != nil {
150+
return nil, err
151+
}
152+
}
153+
return d.storageClient.List(prefix)
133154
}
134155

135-
// Copy is not yet implemented in this refactoring
136156
func (d *DavBlobstore) Copy(srcBlob string, dstBlob string) error {
137-
return fmt.Errorf("Copy not yet implemented")
157+
slog.Info("copying blob on webdav", "src", srcBlob, "dst", dstBlob)
158+
if err := validateBlobID(srcBlob); err != nil {
159+
return fmt.Errorf("invalid source blob ID: %w", err)
160+
}
161+
if err := validateBlobID(dstBlob); err != nil {
162+
return fmt.Errorf("invalid destination blob ID: %w", err)
163+
}
164+
return d.storageClient.Copy(srcBlob, dstBlob)
138165
}
139166

140-
// Properties is not yet implemented in this refactoring
141167
func (d *DavBlobstore) Properties(dest string) error {
142-
return fmt.Errorf("Properties not yet implemented")
168+
slog.Info("fetching blob properties from webdav", "dest", dest)
169+
if err := validateBlobID(dest); err != nil {
170+
return err
171+
}
172+
return d.storageClient.Properties(dest)
143173
}
144174

145-
// EnsureStorageExists is not yet implemented in this refactoring
146175
func (d *DavBlobstore) EnsureStorageExists() error {
147-
return fmt.Errorf("EnsureStorageExists not yet implemented")
176+
slog.Info("ensuring webdav storage root exists")
177+
return d.storageClient.EnsureStorageExists()
148178
}

dav/client/client_test.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"io"
66
"os"
77
"strings"
8+
"time"
89

910
"github.com/cloudfoundry/storage-cli/dav/client"
1011
"github.com/cloudfoundry/storage-cli/dav/client/clientfakes"
@@ -124,4 +125,193 @@ var _ = Describe("Client", func() {
124125
Expect(exists).To(BeFalse())
125126
})
126127
})
128+
129+
Context("Sign", func() {
130+
var expiry = 100 * time.Second
131+
132+
It("returns a signed URL for action 'get'", func() {
133+
fakeStorageClient := &clientfakes.FakeStorageClient{}
134+
fakeStorageClient.SignReturns("https://the-signed-url", nil)
135+
136+
davBlobstore := client.NewWithStorageClient(fakeStorageClient)
137+
url, err := davBlobstore.Sign("blob/path", "get", expiry)
138+
139+
Expect(err).NotTo(HaveOccurred())
140+
Expect(url).To(Equal("https://the-signed-url"))
141+
142+
Expect(fakeStorageClient.SignCallCount()).To(Equal(1))
143+
object, action, expiration := fakeStorageClient.SignArgsForCall(0)
144+
Expect(object).To(Equal("blob/path"))
145+
Expect(action).To(Equal("GET"))
146+
Expect(expiration).To(Equal(expiry))
147+
})
148+
149+
It("returns a signed URL for action 'put'", func() {
150+
fakeStorageClient := &clientfakes.FakeStorageClient{}
151+
fakeStorageClient.SignReturns("https://the-signed-url", nil)
152+
153+
davBlobstore := client.NewWithStorageClient(fakeStorageClient)
154+
url, err := davBlobstore.Sign("blob/path", "put", expiry)
155+
156+
Expect(err).NotTo(HaveOccurred())
157+
Expect(url).To(Equal("https://the-signed-url"))
158+
159+
_, action, _ := fakeStorageClient.SignArgsForCall(0)
160+
Expect(action).To(Equal("PUT"))
161+
})
162+
163+
It("fails on unknown action without calling the storage client", func() {
164+
fakeStorageClient := &clientfakes.FakeStorageClient{}
165+
166+
davBlobstore := client.NewWithStorageClient(fakeStorageClient)
167+
url, err := davBlobstore.Sign("blob/path", "unknown", expiry)
168+
169+
Expect(err).To(HaveOccurred())
170+
Expect(err.Error()).To(ContainSubstring("action not implemented"))
171+
Expect(url).To(Equal(""))
172+
Expect(fakeStorageClient.SignCallCount()).To(Equal(0))
173+
})
174+
175+
It("propagates errors from the storage client", func() {
176+
fakeStorageClient := &clientfakes.FakeStorageClient{}
177+
fakeStorageClient.SignReturns("", fmt.Errorf("boom"))
178+
179+
davBlobstore := client.NewWithStorageClient(fakeStorageClient)
180+
url, err := davBlobstore.Sign("blob/path", "get", expiry)
181+
182+
Expect(err).To(HaveOccurred())
183+
Expect(err.Error()).To(ContainSubstring("boom"))
184+
Expect(url).To(Equal(""))
185+
})
186+
})
187+
188+
Context("Copy", func() {
189+
It("forwards source and destination to the storage client", func() {
190+
fakeStorageClient := &clientfakes.FakeStorageClient{}
191+
fakeStorageClient.CopyReturns(nil)
192+
193+
davBlobstore := client.NewWithStorageClient(fakeStorageClient)
194+
err := davBlobstore.Copy("src/blob", "dst/blob")
195+
196+
Expect(err).NotTo(HaveOccurred())
197+
Expect(fakeStorageClient.CopyCallCount()).To(Equal(1))
198+
199+
src, dst := fakeStorageClient.CopyArgsForCall(0)
200+
Expect(src).To(Equal("src/blob"))
201+
Expect(dst).To(Equal("dst/blob"))
202+
})
203+
204+
It("propagates errors from the storage client", func() {
205+
fakeStorageClient := &clientfakes.FakeStorageClient{}
206+
fakeStorageClient.CopyReturns(fmt.Errorf("copy failed"))
207+
208+
davBlobstore := client.NewWithStorageClient(fakeStorageClient)
209+
err := davBlobstore.Copy("src/blob", "dst/blob")
210+
211+
Expect(err).To(HaveOccurred())
212+
Expect(err.Error()).To(ContainSubstring("copy failed"))
213+
})
214+
})
215+
216+
Context("List", func() {
217+
It("returns the blobs reported by the storage client", func() {
218+
fakeStorageClient := &clientfakes.FakeStorageClient{}
219+
fakeStorageClient.ListReturns([]string{"a/b/c", "a/b/d"}, nil)
220+
221+
davBlobstore := client.NewWithStorageClient(fakeStorageClient)
222+
blobs, err := davBlobstore.List("a/b")
223+
224+
Expect(err).NotTo(HaveOccurred())
225+
Expect(blobs).To(ConsistOf("a/b/c", "a/b/d"))
226+
227+
Expect(fakeStorageClient.ListCallCount()).To(Equal(1))
228+
Expect(fakeStorageClient.ListArgsForCall(0)).To(Equal("a/b"))
229+
})
230+
231+
It("propagates errors from the storage client", func() {
232+
fakeStorageClient := &clientfakes.FakeStorageClient{}
233+
fakeStorageClient.ListReturns(nil, fmt.Errorf("list failed"))
234+
235+
davBlobstore := client.NewWithStorageClient(fakeStorageClient)
236+
blobs, err := davBlobstore.List("any/prefix")
237+
238+
Expect(err).To(HaveOccurred())
239+
Expect(err.Error()).To(ContainSubstring("list failed"))
240+
Expect(blobs).To(BeNil())
241+
})
242+
})
243+
244+
Context("DeleteRecursive", func() {
245+
It("forwards the prefix to the storage client", func() {
246+
fakeStorageClient := &clientfakes.FakeStorageClient{}
247+
fakeStorageClient.DeleteRecursiveReturns(nil)
248+
249+
davBlobstore := client.NewWithStorageClient(fakeStorageClient)
250+
err := davBlobstore.DeleteRecursive("some/prefix")
251+
252+
Expect(err).NotTo(HaveOccurred())
253+
Expect(fakeStorageClient.DeleteRecursiveCallCount()).To(Equal(1))
254+
Expect(fakeStorageClient.DeleteRecursiveArgsForCall(0)).To(Equal("some/prefix"))
255+
})
256+
257+
It("propagates errors from the storage client", func() {
258+
fakeStorageClient := &clientfakes.FakeStorageClient{}
259+
fakeStorageClient.DeleteRecursiveReturns(fmt.Errorf("recursive delete failed"))
260+
261+
davBlobstore := client.NewWithStorageClient(fakeStorageClient)
262+
err := davBlobstore.DeleteRecursive("some/prefix")
263+
264+
Expect(err).To(HaveOccurred())
265+
Expect(err.Error()).To(ContainSubstring("recursive delete failed"))
266+
})
267+
})
268+
269+
Context("Properties", func() {
270+
It("forwards the destination to the storage client", func() {
271+
fakeStorageClient := &clientfakes.FakeStorageClient{}
272+
fakeStorageClient.PropertiesReturns(nil)
273+
274+
davBlobstore := client.NewWithStorageClient(fakeStorageClient)
275+
err := davBlobstore.Properties("blob/path")
276+
277+
Expect(err).NotTo(HaveOccurred())
278+
Expect(fakeStorageClient.PropertiesCallCount()).To(Equal(1))
279+
Expect(fakeStorageClient.PropertiesArgsForCall(0)).To(Equal("blob/path"))
280+
})
281+
282+
It("propagates errors from the storage client", func() {
283+
fakeStorageClient := &clientfakes.FakeStorageClient{}
284+
fakeStorageClient.PropertiesReturns(fmt.Errorf("properties failed"))
285+
286+
davBlobstore := client.NewWithStorageClient(fakeStorageClient)
287+
err := davBlobstore.Properties("blob/path")
288+
289+
Expect(err).To(HaveOccurred())
290+
Expect(err.Error()).To(ContainSubstring("properties failed"))
291+
})
292+
})
293+
294+
Context("EnsureStorageExists", func() {
295+
It("delegates to the storage client", func() {
296+
fakeStorageClient := &clientfakes.FakeStorageClient{}
297+
fakeStorageClient.EnsureStorageExistsReturns(nil)
298+
299+
davBlobstore := client.NewWithStorageClient(fakeStorageClient)
300+
err := davBlobstore.EnsureStorageExists()
301+
302+
Expect(err).NotTo(HaveOccurred())
303+
Expect(fakeStorageClient.EnsureStorageExistsCallCount()).To(Equal(1))
304+
})
305+
306+
It("propagates errors from the storage client", func() {
307+
fakeStorageClient := &clientfakes.FakeStorageClient{}
308+
fakeStorageClient.EnsureStorageExistsReturns(fmt.Errorf("ensure failed"))
309+
310+
davBlobstore := client.NewWithStorageClient(fakeStorageClient)
311+
err := davBlobstore.EnsureStorageExists()
312+
313+
Expect(err).To(HaveOccurred())
314+
Expect(err.Error()).To(ContainSubstring("ensure failed"))
315+
})
316+
})
127317
})

0 commit comments

Comments
 (0)