Skip to content

Commit 60aed97

Browse files
authored
Merge pull request #726 from nono/worker-unzip
Worker unzip
2 parents 9fa5054 + 6b5138a commit 60aed97

File tree

8 files changed

+241
-2
lines changed

8 files changed

+241
-2
lines changed

docs/workers.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,42 @@ arguments.
1212
The `log` worker will just print in the log file the job sent to it. It can
1313
useful for debugging for example.
1414

15+
## unzip worker
16+
17+
The `unzip` worker can take a zip archive from the VFS, and will unzip the
18+
files inside it to a directory of the VFS. The options are:
19+
20+
- `zip`: the ID of the zip file
21+
- `destination`: the ID of the directory where the files will be unzipped.
22+
23+
### Example
24+
25+
```json
26+
{
27+
"zip": "8737b5d6-51b6-11e7-9194-bf5b64b3bc9e",
28+
"destination": "88750a84-51b6-11e7-ba90-4f0b1cb62b7b"
29+
}
30+
```
31+
32+
### Permissions
33+
34+
To use this worker from a client-side application, you will need to ask the
35+
permission. It is done by adding this to the manifest:
36+
37+
```json
38+
{
39+
"permissions": {
40+
"unzip-to-a-directory": {
41+
"description": "Required to unzip a file inside the cozy",
42+
"type": "io.cozy.jobs",
43+
"verbs": ["POST"],
44+
"selector": "worker",
45+
"values": ["unzip"]
46+
}
47+
}
48+
}
49+
```
50+
1551
## sendmail worker
1652

1753
The `sendmail` worker can be used to send mail from the stack. It implies that

pkg/vfs/vfs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ type Fs interface {
7878
// file for reading or writing.
7979
type File interface {
8080
io.Reader
81+
io.ReaderAt
8182
io.Seeker
8283
io.Writer
8384
io.Closer

pkg/vfs/vfsafero/impl.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,10 @@ func (f *aferoFileOpen) Read(p []byte) (int, error) {
407407
return f.f.Read(p)
408408
}
409409

410+
func (f *aferoFileOpen) ReadAt(p []byte, off int64) (int, error) {
411+
return f.f.ReadAt(p, off)
412+
}
413+
410414
func (f *aferoFileOpen) Seek(offset int64, whence int) (int64, error) {
411415
return f.f.Seek(offset, whence)
412416
}
@@ -441,6 +445,10 @@ func (f *aferoFileCreation) Read(p []byte) (int, error) {
441445
return 0, os.ErrInvalid
442446
}
443447

448+
func (f *aferoFileCreation) ReadAt(p []byte, off int64) (int, error) {
449+
return 0, os.ErrInvalid
450+
}
451+
444452
func (f *aferoFileCreation) Seek(offset int64, whence int) (int64, error) {
445453
return 0, os.ErrInvalid
446454
}

pkg/vfs/vfsswift/impl.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package vfsswift
22

33
import (
4+
"bytes"
45
"encoding/hex"
56
"fmt"
67
"io"
8+
"io/ioutil"
79
"os"
810
"strconv"
911
"strings"
@@ -329,7 +331,7 @@ func (sfs *swiftVFS) OpenFile(doc *vfs.FileDoc) (vfs.File, error) {
329331
if err != nil {
330332
return nil, err
331333
}
332-
return &swiftFileOpen{f}, nil
334+
return &swiftFileOpen{f, nil}, nil
333335
}
334336

335337
// UpdateFileDoc overrides the indexer's one since the swift fs indexes files
@@ -460,6 +462,10 @@ func (f *swiftFileCreation) Read(p []byte) (int, error) {
460462
return 0, os.ErrInvalid
461463
}
462464

465+
func (f *swiftFileCreation) ReadAt(p []byte, off int64) (int, error) {
466+
return 0, os.ErrInvalid
467+
}
468+
463469
func (f *swiftFileCreation) Seek(offset int64, whence int) (int64, error) {
464470
return 0, os.ErrInvalid
465471
}
@@ -584,13 +590,26 @@ func (f *swiftFileCreation) Close() (err error) {
584590
}
585591

586592
type swiftFileOpen struct {
587-
f *swift.ObjectOpenFile
593+
f *swift.ObjectOpenFile
594+
br *bytes.Reader
588595
}
589596

590597
func (f *swiftFileOpen) Read(p []byte) (int, error) {
591598
return f.f.Read(p)
592599
}
593600

601+
func (f *swiftFileOpen) ReadAt(p []byte, off int64) (int, error) {
602+
// TODO find something smarter than keeping the whole file in memory
603+
if f.br == nil {
604+
buf, err := ioutil.ReadAll(f.f)
605+
if err != nil {
606+
return 0, err
607+
}
608+
f.br = bytes.NewReader(buf)
609+
}
610+
return f.br.ReadAt(p, off)
611+
}
612+
594613
func (f *swiftFileOpen) Seek(offset int64, whence int) (int64, error) {
595614
return f.f.Seek(offset, whence)
596615
}

pkg/workers/unzip/unzip.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package unzip
2+
3+
import (
4+
"archive/zip"
5+
"context"
6+
"fmt"
7+
"io"
8+
"path"
9+
"runtime"
10+
"time"
11+
12+
"github.com/cozy/cozy-stack/pkg/couchdb"
13+
"github.com/cozy/cozy-stack/pkg/instance"
14+
"github.com/cozy/cozy-stack/pkg/jobs"
15+
"github.com/cozy/cozy-stack/pkg/logger"
16+
"github.com/cozy/cozy-stack/pkg/vfs"
17+
)
18+
19+
type zipMessage struct {
20+
Zip string `json:"zip"`
21+
Destination string `string:"destination"`
22+
}
23+
24+
func init() {
25+
jobs.AddWorker("unzip", &jobs.WorkerConfig{
26+
Concurrency: (runtime.NumCPU() + 1) / 2,
27+
MaxExecCount: 2,
28+
Timeout: 30 * time.Second,
29+
WorkerFunc: Worker,
30+
})
31+
}
32+
33+
// Worker is a worker that unzip a file.
34+
func Worker(ctx context.Context, m *jobs.Message) error {
35+
msg := &zipMessage{}
36+
if err := m.Unmarshal(msg); err != nil {
37+
return err
38+
}
39+
domain := ctx.Value(jobs.ContextDomainKey).(string)
40+
log := logger.WithDomain(domain)
41+
log.Infof("[jobs] unzip %s in %s", msg.Zip, msg.Destination)
42+
i, err := instance.Get(domain)
43+
if err != nil {
44+
return err
45+
}
46+
fs := i.VFS()
47+
return unzip(fs, msg.Zip, msg.Destination)
48+
}
49+
50+
func unzip(fs vfs.VFS, zipID, destination string) error {
51+
zipDoc, err := fs.FileByID(zipID)
52+
if err != nil {
53+
return err
54+
}
55+
dstDoc, err := fs.DirByID(destination)
56+
if err != nil {
57+
return err
58+
}
59+
60+
fr, err := fs.OpenFile(zipDoc)
61+
if err != nil {
62+
return err
63+
}
64+
defer fr.Close()
65+
r, err := zip.NewReader(fr, zipDoc.ByteSize)
66+
if err != nil {
67+
return err
68+
}
69+
for _, f := range r.File {
70+
name := path.Base(f.Name)
71+
dirname := path.Dir(f.Name)
72+
dir := dstDoc
73+
if dirname != "." {
74+
dirname = path.Join(dstDoc.Fullpath, dirname)
75+
dir, err = vfs.MkdirAll(fs, dirname, nil)
76+
if err != nil {
77+
return err
78+
}
79+
}
80+
81+
rc, err := f.Open()
82+
if err != nil {
83+
return err
84+
}
85+
86+
size := int64(f.UncompressedSize64)
87+
mime, class := vfs.ExtractMimeAndClassFromFilename(f.Name)
88+
now := time.Now()
89+
doc, err := vfs.NewFileDoc(name, dir.ID(), size, nil, mime, class, now, false, false, nil)
90+
if err != nil {
91+
return err
92+
}
93+
file, err := fs.CreateFile(doc, nil)
94+
if err != nil {
95+
if couchdb.IsConflictError(err) {
96+
doc.DocName = fmt.Sprintf("%s - conflict - %d", doc.DocName, time.Now().Unix())
97+
file, err = fs.CreateFile(doc, nil)
98+
if err != nil {
99+
return err
100+
}
101+
}
102+
}
103+
_, err = io.Copy(file, rc)
104+
cerr := file.Close()
105+
if err != nil {
106+
return err
107+
}
108+
if cerr != nil {
109+
return cerr
110+
}
111+
}
112+
return nil
113+
}

pkg/workers/unzip/unzip_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package unzip
2+
3+
import (
4+
"io"
5+
"os"
6+
"testing"
7+
"time"
8+
9+
"github.com/cozy/cozy-stack/pkg/config"
10+
"github.com/cozy/cozy-stack/pkg/consts"
11+
"github.com/cozy/cozy-stack/pkg/instance"
12+
"github.com/cozy/cozy-stack/pkg/vfs"
13+
"github.com/cozy/cozy-stack/tests/testutils"
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
var inst *instance.Instance
18+
19+
func TestUnzip(t *testing.T) {
20+
fs := inst.VFS()
21+
dst, err := vfs.Mkdir(fs, "/destination", nil)
22+
assert.NoError(t, err)
23+
_, err = vfs.Mkdir(fs, "/destination/foo", nil)
24+
assert.NoError(t, err)
25+
26+
fd, err := os.Open("../../../tests/fixtures/logos.zip")
27+
assert.NoError(t, err)
28+
defer fd.Close()
29+
zip, err := vfs.NewFileDoc("logos.zip", consts.RootDirID, -1, nil, "application/zip", "application", time.Now(), false, false, nil)
30+
assert.NoError(t, err)
31+
file, err := fs.CreateFile(zip, nil)
32+
assert.NoError(t, err)
33+
_, err = io.Copy(file, fd)
34+
assert.NoError(t, err)
35+
assert.NoError(t, file.Close())
36+
37+
_, err = fs.OpenFile(zip)
38+
assert.NoError(t, err)
39+
40+
err = unzip(fs, zip.ID(), dst.ID())
41+
assert.NoError(t, err)
42+
43+
blue, err := fs.FileByPath("/destination/blue.svg")
44+
assert.NoError(t, err)
45+
assert.Equal(t, int64(2029), blue.ByteSize)
46+
47+
white, err := fs.FileByPath("/destination/white.svg")
48+
assert.NoError(t, err)
49+
assert.Equal(t, int64(2030), white.ByteSize)
50+
51+
baz, err := fs.FileByPath("/destination/foo/bar/baz")
52+
assert.NoError(t, err)
53+
assert.Equal(t, int64(4), baz.ByteSize)
54+
}
55+
56+
func TestMain(m *testing.M) {
57+
config.UseTestFile()
58+
setup := testutils.NewSetup(m, "unzip_test")
59+
inst = setup.GetTestInstance()
60+
os.Exit(setup.Run())
61+
}

tests/fixtures/logos.zip

2.75 KB
Binary file not shown.

web/jobs/jobs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
_ "github.com/cozy/cozy-stack/pkg/workers/mails"
2121
_ "github.com/cozy/cozy-stack/pkg/workers/sharings"
2222
_ "github.com/cozy/cozy-stack/pkg/workers/thumbnail"
23+
_ "github.com/cozy/cozy-stack/pkg/workers/unzip"
2324
)
2425

2526
type (

0 commit comments

Comments
 (0)