Skip to content

Commit 9380a49

Browse files
committed
feat(volumes): add volumes serve
1 parent 4973687 commit 9380a49

8 files changed

Lines changed: 94 additions & 363 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.25
55
require (
66
github.com/chai2010/gettext-go v1.0.3
77
github.com/containerd/console v1.0.4
8-
github.com/drycc/controller-sdk-go v0.0.0-20250930075415-578495a55414
8+
github.com/drycc/controller-sdk-go v0.0.0-20251203063317-ba04d7d3aa6a
99
github.com/drycc/pkg v0.0.0-20250917064731-345368da3dbf
1010
github.com/minio/selfupdate v0.6.0
1111
github.com/olekukonko/tablewriter v0.0.5

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
1111
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
1212
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
1313
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14-
github.com/drycc/controller-sdk-go v0.0.0-20250930075415-578495a55414 h1:wiVKgwQsSEBbrQj92Lor6YknaAwPUVp1s/VmAmS3/lI=
15-
github.com/drycc/controller-sdk-go v0.0.0-20250930075415-578495a55414/go.mod h1:eHcmYwg81ASlP55/U587xnBZnZoeZnPHXGeQ8nYWnsg=
14+
github.com/drycc/controller-sdk-go v0.0.0-20251203063317-ba04d7d3aa6a h1:rM0tLoJnP37VuAz2yVyjCNmUoOZ53x2wvl6blg9Q6HY=
15+
github.com/drycc/controller-sdk-go v0.0.0-20251203063317-ba04d7d3aa6a/go.mod h1:eHcmYwg81ASlP55/U587xnBZnZoeZnPHXGeQ8nYWnsg=
1616
github.com/drycc/pkg v0.0.0-20250917064731-345368da3dbf h1:CYy3NoPhfFhkGAbEppTOQfY/HC2s0FJDcBgbtRKeweg=
1717
github.com/drycc/pkg v0.0.0-20250917064731-345368da3dbf/go.mod h1:BrrNrNskHKm+nJYhXfGuI114w8nupi0AMo8QZHID7CM=
1818
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=

internal/commands/volumes.go

Lines changed: 38 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
package commands
22

33
import (
4+
"context"
45
"fmt"
5-
"io"
6-
"net/http"
76
"net/url"
87
"os"
9-
"path"
8+
"os/signal"
109
"regexp"
1110
"strings"
11+
"syscall"
1212
"time"
1313

14-
drycc "github.com/drycc/controller-sdk-go"
1514
"github.com/drycc/controller-sdk-go/api"
1615
"github.com/drycc/controller-sdk-go/volumes"
1716
"github.com/drycc/workflow-cli/internal/loader"
@@ -161,17 +160,41 @@ func (d *DryccCmd) VolumesDelete(appID, name string) error {
161160
return nil
162161
}
163162

164-
// VolumesClient a client for manage volume file
165-
func (d *DryccCmd) VolumesClient(appID, cmd string, args ...string) error {
166-
switch cmd {
167-
case "ls":
168-
return d.volumesClientLs(appID, args[0])
169-
case "cp":
170-
return d.volumesClientCp(appID, args[0], args[1])
171-
case "rm":
172-
return d.volumesClientRm(appID, args[0])
173-
default:
174-
return fmt.Errorf("unknown command %s", cmd)
163+
// VolumesServe Serve serves an app's volume.
164+
func (d *DryccCmd) VolumesServe(appID, name string) error {
165+
appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
166+
if err != nil {
167+
return err
168+
}
169+
parent, cancel := context.WithCancel(context.Background())
170+
defer cancel()
171+
quit := progress(d.WOut)
172+
ctx, filer, err := volumes.Serve(parent, s.Client, appID, name)
173+
quit <- true
174+
<-quit
175+
if err != nil {
176+
return err
177+
}
178+
179+
table := d.getDefaultFormatTable([]string{})
180+
table.Append([]string{"Endpoint:", filer["endpoint"]})
181+
table.Append([]string{"Username:", filer["username"]})
182+
table.Append([]string{"Password:", filer["password"]})
183+
table.Render()
184+
d.Print("\n")
185+
186+
signalChan := make(chan os.Signal, 1)
187+
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP, syscall.SIGUSR1, syscall.SIGUSR2)
188+
d.Printf("WebDAV service for volume %s is running. Press Ctrl+C to stop.\n", name)
189+
for {
190+
select {
191+
case <-signalChan:
192+
return nil
193+
case <-ctx.Done():
194+
return nil
195+
default:
196+
time.Sleep(2 * time.Second)
197+
}
175198
}
176199
}
177200

@@ -233,172 +256,6 @@ func (d *DryccCmd) VolumesUnmount(appID string, name string, volumeVars []string
233256
return nil
234257
}
235258

236-
// volumesClientLs get all directory entries sorted by filename.
237-
func (d *DryccCmd) volumesClientLs(appID, vol string) error {
238-
appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
239-
if err != nil {
240-
return err
241-
}
242-
243-
name, path, err := parseVol(vol)
244-
if err != nil {
245-
return err
246-
}
247-
dirs, _, err := volumes.ListDir(s.Client, appID, name, path, 3000)
248-
if err != nil {
249-
return err
250-
}
251-
252-
table := d.getDefaultFormatTable([]string{})
253-
for _, dir := range dirs {
254-
if dir.Type == "dir" {
255-
dir.Name = fmt.Sprintf("%s/", dir.Name)
256-
}
257-
table.Append([]string{fmt.Sprintf("[%s]", d.formatTime(dir.Timestamp)), dir.Size, dir.Name})
258-
}
259-
table.Render()
260-
return nil
261-
}
262-
263-
func (d *DryccCmd) volumesClientGetAll(client *drycc.Client, appID, volumeID, volumePath, localPath string) error {
264-
if _, err := os.Stat(localPath); err != nil && os.IsNotExist(err) {
265-
os.MkdirAll(localPath, os.ModePerm)
266-
}
267-
dirs, _, err := volumes.ListDir(client, appID, volumeID, volumePath, 3000)
268-
if err != nil {
269-
return err
270-
}
271-
for _, dir := range dirs {
272-
_, subpath := path.Split(dir.Path)
273-
filepath := path.Join(localPath, subpath)
274-
if dir.Type == "file" {
275-
res, err := volumes.GetFile(client, appID, volumeID, dir.Path)
276-
if err != nil {
277-
return err
278-
}
279-
w, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
280-
if err != nil {
281-
return err
282-
}
283-
bar := d.newProgressbar(res.ContentLength, "↓", filepath)
284-
defer w.Close()
285-
if _, err = io.Copy(io.MultiWriter(w, bar), res.Body); err != nil {
286-
return err
287-
}
288-
} else {
289-
os.MkdirAll(filepath, os.ModePerm)
290-
if err := d.volumesClientGetAll(client, appID, volumeID, dir.Path, filepath); err != nil {
291-
return err
292-
}
293-
}
294-
}
295-
return nil
296-
}
297-
298-
func (d *DryccCmd) volumesClientPostAll(client *drycc.Client, appID, volumeID, volumePath, localPath string) error {
299-
if file, err := os.Stat(localPath); err != nil {
300-
return err
301-
} else if !file.IsDir() {
302-
file, err := os.Open(localPath)
303-
if err != nil {
304-
return err
305-
}
306-
defer file.Close()
307-
308-
stat, err := file.Stat()
309-
if err != nil {
310-
return err
311-
}
312-
if stat.Size() > 0 { // ignore empty file
313-
reader := progressbar.NewReader(file, d.newProgressbar(stat.Size(), "↑", localPath))
314-
if _, err := volumes.PostFile(client, appID, volumeID, volumePath, file.Name(), stat.Size(), &reader); err != nil {
315-
return err
316-
}
317-
} else {
318-
d.newProgressbar(1, "?", localPath).Finish()
319-
}
320-
return nil
321-
}
322-
if entries, err := os.ReadDir(localPath); err == nil {
323-
for _, entry := range entries {
324-
var dstFilepath string
325-
if entry.IsDir() {
326-
dstFilepath = path.Join(volumePath, entry.Name())
327-
} else {
328-
dstFilepath = volumePath
329-
}
330-
if err := d.volumesClientPostAll(client, appID, volumeID, dstFilepath, path.Join(localPath, entry.Name())); err != nil {
331-
return err
332-
}
333-
}
334-
} else {
335-
return err
336-
}
337-
return nil
338-
}
339-
340-
// volumesClientCp copy files between volume and local file
341-
func (d *DryccCmd) volumesClientCp(appID, src, dst string) error {
342-
appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
343-
if err != nil {
344-
return err
345-
}
346-
if strings.HasPrefix(src, "vol://") {
347-
f, err := os.Stat(dst)
348-
if err != nil {
349-
return err
350-
}
351-
if !f.IsDir() {
352-
return fmt.Errorf("the local path must be an existing dir")
353-
}
354-
volumeID, volumePath, err := parseVol(src)
355-
if err != nil {
356-
return err
357-
}
358-
if dirs, _, err := volumes.ListDir(s.Client, appID, volumeID, volumePath, 3000); err == nil && (len(dirs) != 1 || dirs[0].Type != "file") {
359-
dst = mergeDestDir(dst, volumePath)
360-
}
361-
return d.volumesClientGetAll(s.Client, appID, volumeID, volumePath, dst)
362-
} else if strings.HasPrefix(dst, "vol://") {
363-
volumeID, volumePath, err := parseVol(dst)
364-
if err != nil {
365-
return err
366-
}
367-
if dirs, _, err := volumes.ListDir(s.Client, appID, volumeID, volumePath, 3000); err == nil {
368-
names := strings.Split(strings.Trim(src, "/"), "/")
369-
if len(dirs) == 1 && dirs[0].Type == "file" && strings.HasSuffix(strings.Trim(volumePath, "/"), names[len(names)-1]) {
370-
return fmt.Errorf("the volume path cannot be an existing file")
371-
}
372-
}
373-
if file, err := os.Stat(src); err == nil && file.IsDir() {
374-
volumePath = mergeDestDir(volumePath, src)
375-
}
376-
return d.volumesClientPostAll(s.Client, appID, volumeID, volumePath, src)
377-
}
378-
return nil
379-
}
380-
381-
// volumesClientRm delete a file from volume
382-
func (d *DryccCmd) volumesClientRm(appID, vol string) error {
383-
appID, s, err := loader.LoadAppSettings(d.ConfigFile, appID)
384-
if err != nil {
385-
return err
386-
}
387-
host, path, err := parseVol(vol)
388-
if err != nil {
389-
return err
390-
}
391-
res, err := volumes.DeleteFile(s.Client, appID, host, path)
392-
if err != nil {
393-
return err
394-
}
395-
if res.StatusCode != http.StatusOK {
396-
return fmt.Errorf("incorrect http status code %d", res.StatusCode)
397-
}
398-
399-
return nil
400-
}
401-
402259
func parseVolume(volumeVars []string) (map[string]any, error) {
403260
volumeMap := make(map[string]any)
404261
regex := regexp.MustCompile(`^([a-z0-9]+(?:-[a-z0-9]+)*)=(\/([\w]+[\w-]*\/?)+)$`)

internal/commands/volumes_test.go

Lines changed: 34 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import (
44
"bytes"
55
"fmt"
66
"net/http"
7-
"os"
7+
"syscall"
88
"testing"
9+
"time"
910

1011
"github.com/drycc/controller-sdk-go/api"
1112
"github.com/drycc/workflow-cli/pkg/testutil"
@@ -134,7 +135,7 @@ Updated: 2020-08-26T00:00:00UTC
134135
`)
135136
}
136137

137-
func TestVolumesClientLs(t *testing.T) {
138+
func TestVolumesServe(t *testing.T) {
138139
t.Parallel()
139140
cf, server, err := testutil.NewTestServerAndClient()
140141
if err != nil {
@@ -143,68 +144,43 @@ func TestVolumesClientLs(t *testing.T) {
143144
defer server.Close()
144145
var b bytes.Buffer
145146
cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
146-
server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/client/", func(w http.ResponseWriter, _ *http.Request) {
147+
server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/filer/_/ping", func(w http.ResponseWriter, _ *http.Request) {
147148
testutil.SetHeaders(w)
148-
fmt.Fprintf(w, `{"results": [
149-
{"name":"handler.go","size":"4159","timestamp":"2024-06-25T22:55:16+08:00","type":"file","path":"/handler.go"},
150-
{"name":"handler_test.go","size":"2310","timestamp":"2024-06-04T15:29:45+08:00","type":"file","path":"/handler_test.go"}
151-
], "count": 2}`)
149+
fmt.Fprintf(w, `pong`)
152150
})
153-
154-
err = cmdr.VolumesClient("example-go", "ls", "vol://myvolume")
155-
assert.NoError(t, err)
156-
assert.Contains(t, b.String(), "handler_test.go")
157-
}
158-
159-
func TestVolumesClientCp(t *testing.T) {
160-
t.Parallel()
161-
cf, server, err := testutil.NewTestServerAndClient()
162-
if err != nil {
163-
t.Fatal(err)
164-
}
165-
server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/client/", func(w http.ResponseWriter, r *http.Request) {
166-
testutil.SetHeaders(w)
167-
if r.URL.RawQuery == "path=etc" {
168-
fmt.Fprintf(w, `{"results":[],"count":0}`)
169-
} else if r.Method == http.MethodGet {
170-
fmt.Fprintf(w, `{"results":[{"name":"hello.txt","size":"4159","timestamp":"2024-06-25T22:55:16+08:00","type":"file","path":"/hello.txt"}], "count": 1}`)
171-
}
172-
})
173-
server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/client/hello.txt", func(w http.ResponseWriter, _ *http.Request) {
151+
server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/filer/_/bind", func(w http.ResponseWriter, _ *http.Request) {
174152
testutil.SetHeaders(w)
175-
fmt.Fprintf(w, `hello word`)
153+
fmt.Fprintf(w, `{"endpoint": "/v2/apps/example-go/volumes/myvolume/filer/webdav", "username": "user", "password": "pass"}`)
176154
})
177-
defer server.Close()
178155

179-
var b bytes.Buffer
180-
cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
181-
// test download file
182-
err = cmdr.VolumesClient("example-go", "cp", "vol://myvolume/hello.txt", "/tmp")
183-
assert.NoError(t, err)
184-
result, err := os.ReadFile("/tmp/hello.txt")
185-
assert.NoError(t, err)
186-
assert.Equal(t, string(result), `hello word`, "output")
187-
// test upload file
188-
err = cmdr.VolumesClient("example-go", "cp", "/tmp/hello.txt", "vol://myvolume/etc")
189-
assert.NoError(t, err)
190-
}
191-
192-
func TestVolumesClientRm(t *testing.T) {
193-
t.Parallel()
194-
cf, server, err := testutil.NewTestServerAndClient()
195-
if err != nil {
196-
t.Fatal(err)
156+
// Use a channel to signal when the method has been called
157+
done := make(chan error, 1)
158+
go func() {
159+
done <- cmdr.VolumesServe("example-go", "myvolume")
160+
}()
161+
162+
// Give the goroutine time to start and produce output
163+
time.Sleep(500 * time.Millisecond)
164+
165+
// Send interrupt signal to stop the blocking method
166+
go func() {
167+
time.Sleep(100 * time.Millisecond)
168+
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
169+
}()
170+
171+
// Wait for completion with timeout
172+
select {
173+
case err := <-done:
174+
assert.NoError(t, err)
175+
case <-time.After(5 * time.Second):
176+
t.Fatal("VolumesServe did not complete within timeout")
197177
}
198-
defer server.Close()
199-
var b bytes.Buffer
200-
cmdr := DryccCmd{WOut: &b, ConfigFile: cf}
201-
// test rm file
202-
server.Mux.HandleFunc("/v2/apps/example-go/volumes/myvolume/client/etc/hello.txt", func(w http.ResponseWriter, _ *http.Request) {
203-
testutil.SetHeaders(w)
204-
w.WriteHeader(http.StatusOK)
205-
})
206-
err = cmdr.VolumesClient("example-go", "rm", "vol://myvolume/etc/hello.txt")
207-
assert.NoError(t, err)
178+
179+
output := b.String()
180+
assert.Contains(t, output, "Starting WebDAV service")
181+
assert.Contains(t, output, "Endpoint:")
182+
assert.Contains(t, output, "Username:")
183+
assert.Contains(t, output, "Password:")
208184
}
209185

210186
func TestVolumesExpand(t *testing.T) {

0 commit comments

Comments
 (0)