Repository navigation
Expand file tree
/
Copy pathattach.go
More file actions
159 lines (147 loc) · 4.36 KB
/
Copy pathattach.go
File metadata and controls
159 lines (147 loc) · 4.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
package main
import (
"cmp"
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"golang.org/x/sys/unix"
"google.golang.org/api/chat/v1"
)
// file is something in a message that /open and /save can act on.
type file struct {
label string
media string // Chat media resource name, for uploaded files
url string // browser URL, for Drive files and GIFs
}
func filesOf(m *chat.Message) []file {
out := make([]file, 0, len(m.Attachment)+len(m.AttachedGifs))
for _, a := range m.Attachment {
f := file{label: cmp.Or(a.ContentName, a.ContentType, "attachment")}
switch {
case a.AttachmentDataRef != nil:
f.media = a.AttachmentDataRef.ResourceName
case a.DriveDataRef != nil:
f.url = "https://drive.google.com/open?id=" + a.DriveDataRef.DriveFileId
default:
f.url = a.DownloadUri
}
out = append(out, f)
}
for _, g := range m.AttachedGifs {
out = append(out, file{label: "gif", url: g.Uri})
}
return out
}
func threadFiles(t *thread) []file {
out := make([]file, 0, len(t.msgs))
for _, m := range t.msgs {
out = append(out, filesOf(m)...)
}
return out
}
// downloadsDir is where /save puts files. It's never the home directory
// itself, where a file named like a dotfile would run at the next login.
func downloadsDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
d := filepath.Join(home, "Downloads")
return d, os.MkdirAll(d, 0o700)
}
// openDir is where /open puts files before opening them. It's per user,
// because in a shared temp dir another local user could swap the file.
func openDir() (string, error) {
cache, err := os.UserCacheDir()
if err != nil {
return "", err
}
d := filepath.Join(cache, "mutter", "open")
return d, os.MkdirAll(d, 0o700)
}
// saveFile downloads an uploaded file into dir and returns its path.
func (c *client) saveFile(ctx context.Context, f file, dir string) (string, error) {
resp, err := c.svc.Media.Download(f.media).Context(ctx).Download()
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("download %s: %s", f.label, resp.Status)
}
out, path, err := createUnique(dir, safeName(f.label))
if err != nil {
return "", err
}
if _, err := io.Copy(out, resp.Body); err != nil {
_ = out.Close()
_ = os.Remove(path) // a partial download is useless
return "", err
}
if err := out.Close(); err != nil {
return "", err
}
quarantine(path)
return path, nil
}
// safeName keeps a sender-chosen name from escaping the target directory or
// landing as a hidden dotfile.
func safeName(name string) string {
name = strings.TrimLeft(filepath.Base(strings.ReplaceAll(name, `\`, "/")), ".")
if name == "/" || name == "" {
return "attachment"
}
return name
}
// createUnique creates dir/name, or "name (2).ext" and so on when it exists,
// and returns the file and its path. Files open through an os.Root, so a
// name can't reach outside dir even if safeName misses a trick.
func createUnique(dir, name string) (*os.File, string, error) {
root, err := os.OpenRoot(dir)
if err != nil {
return nil, "", err
}
defer root.Close()
ext := filepath.Ext(name)
base := strings.TrimSuffix(name, ext)
for i := 1; i < 1000; i++ {
n := name
if i > 1 {
n = fmt.Sprintf("%s (%d)%s", base, i, ext)
}
f, err := root.OpenFile(n, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if !errors.Is(err, os.ErrExist) {
return f, filepath.Join(dir, n), err
}
}
return nil, "", fmt.Errorf("no free name for %s in %s", name, dir)
}
// quarantine marks a downloaded file the way browsers do, so macOS
// Gatekeeper checks it before it runs. Files come from other people.
func quarantine(path string) {
if runtime.GOOS != darwin {
return
}
v := fmt.Sprintf("0081;%x;mutter;", time.Now().Unix())
_ = unix.Setxattr(path, "com.apple.quarantine", []byte(v), 0)
}
// markUnread moves the space's read state to just before at, so everything
// from at onward is unread on every device.
func (c *client) markUnread(ctx context.Context, space, at string) (string, error) {
t, err := time.Parse(time.RFC3339Nano, at)
if err != nil {
return "", err
}
before := t.Add(-time.Microsecond).UTC().Format(time.RFC3339Nano)
_, err = c.svc.Users.Spaces.UpdateSpaceReadState(readStateName(space), &chat.SpaceReadState{
LastReadTime: before,
}).UpdateMask("lastReadTime").Context(ctx).Do()
return before, err
}