-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathfile.go
More file actions
247 lines (216 loc) · 7.02 KB
/
file.go
File metadata and controls
247 lines (216 loc) · 7.02 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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
package router
import (
"context"
"errors"
"net/http"
"time"
"github.com/google/uuid"
"github.com/labstack/echo/v5"
"github.com/traPtitech/Jomon/internal/logging"
"github.com/traPtitech/Jomon/internal/service"
"go.uber.org/zap"
)
type FileResponse struct {
ID uuid.UUID `json:"id"`
}
type FileMetaResponse struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
MimeType string `json:"mime_type"`
CreatedBy uuid.UUID `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
}
var (
acceptedMimeTypes = map[string]bool{
"image/jpeg": true,
"image/png": true,
"image/gif": true,
"image/bmp": true,
"application/pdf": true,
"application/msword": true,
"application/zip": true,
}
errUserIsNotAccountManagerOrFileCreator = errors.New(
"user is not accountManager or file creator")
)
func (h Handlers) PostFile(c *echo.Context) error {
ctx := c.Request().Context()
logger := logging.GetLogger(ctx)
loginUser, _ := c.Get(loginUserKey).(User)
form, err := c.MultipartForm()
if err != nil {
logger.Error("failed to parse request as multipart/form-data", zap.Error(err))
return service.NewUnexpectedError(err)
}
files, ok := form.File["file"]
if !ok || len(files) != 1 {
logger.Info("could not find field `file` in request, or its length is not 1")
return service.NewBadInputError("invalid file")
}
reqfile := files[0]
names, ok := form.Value["name"]
if !ok || len(names) != 1 {
logger.Info("could not find field `name` in request, or its length is not 1")
return service.NewBadInputError("invalid file name")
}
name := names[0]
applicationIDs, ok := form.Value["application_id"]
if !ok || len(applicationIDs) != 1 {
logger.Info("could not find field `application_id` in request, or its length is not 1")
return service.NewBadInputError("invalid file application id")
}
applicationID, err := uuid.Parse(applicationIDs[0])
if err != nil {
logger.Info("could not parse application_id as UUID", zap.Error(err))
return service.NewBadInputError("invalid file application id").
WithInternal(err)
}
mimetype := reqfile.Header.Get(echo.HeaderContentType)
if !acceptedMimeTypes[mimetype] {
logger.Info("requested unsupported mime type", zap.String("mime-type", mimetype))
return echo.NewHTTPError(
http.StatusUnsupportedMediaType,
"unsupported media type")
}
src, err := reqfile.Open()
if err != nil {
logger.Error("failed to open requested file", zap.Error(err))
return service.NewUnexpectedError(err)
}
defer src.Close()
file, err := h.Repository.CreateFile(ctx, name, mimetype, applicationID, loginUser.ID)
if err != nil {
logger.Error("failed to create file in repository", zap.Error(err))
return service.NewUnexpectedError(err)
}
err = h.Storage.Save(ctx, file.ID.String(), src)
if err != nil {
logger.Error("failed to save file id in storage", zap.Error(err))
// TODO: storageが返すエラーはそのまま返したい
return service.NewUnexpectedError(err)
}
return c.JSON(http.StatusOK, &FileResponse{file.ID})
}
func (h Handlers) GetFile(c *echo.Context) error {
ctx := c.Request().Context()
logger := logging.GetLogger(ctx)
fileID, err := uuid.Parse(c.Param("fileID"))
if err != nil {
logger.Error("could not parse query parameter `fileID` as UUID", zap.Error(err))
return service.NewBadInputError("invalid file ID").
WithInternal(err)
}
file, err := h.Repository.GetFile(ctx, fileID)
if err != nil {
logger.Info(
"file not found in repository",
zap.String("ID", fileID.String()), zap.Error(err))
return err
}
modifiedAt := file.CreatedAt.Truncate(time.Second)
im := c.Request().Header.Get(echo.HeaderIfModifiedSince)
if im != "" {
imt, err := http.ParseTime(im)
if err != nil {
logger.Info("could not parse time in request header", zap.Error(err))
return service.NewBadInputError("invalid If-Modified-Since header").
WithInternal(err)
}
if modifiedAt.Before(imt) || modifiedAt.Equal(imt) {
logger.Info(
"content is not modified since the last request",
zap.String("ID", fileID.String()),
zap.Time("If-Modified-Since", imt))
return c.NoContent(http.StatusNotModified)
}
}
f, err := h.Storage.Open(ctx, fileID.String())
if err != nil {
logger.Error(
"failed to open file in storage",
zap.String("ID", fileID.String()),
zap.Error(err))
return service.NewUnexpectedError(err)
}
defer f.Close()
c.Response().Header().Set("Cache-Control", "private, no-cache, max-age=0")
c.Response().Header().Set(echo.HeaderLastModified, modifiedAt.UTC().Format(http.TimeFormat))
return c.Stream(http.StatusOK, file.MimeType, f)
}
func (h Handlers) GetFileMeta(c *echo.Context) error {
ctx := c.Request().Context()
logger := logging.GetLogger(ctx)
fileID, err := uuid.Parse(c.Param("fileID"))
if err != nil {
logger.Info("could not parse query parameter `fileID` as UUID", zap.Error(err))
return service.NewBadInputError("invalid file ID").
WithInternal(err)
}
file, err := h.Repository.GetFile(ctx, fileID)
if err != nil {
logger.Info(
"file not found in repository",
zap.String("ID", fileID.String()), zap.Error(err))
return err
}
return c.JSON(http.StatusOK, &FileMetaResponse{
ID: file.ID,
Name: file.Name,
MimeType: file.MimeType,
CreatedBy: file.CreatedBy,
CreatedAt: file.CreatedAt,
})
}
func (h Handlers) DeleteFile(c *echo.Context) error {
ctx := c.Request().Context()
logger := logging.GetLogger(ctx)
loginUser, _ := c.Get(loginUserKey).(User)
fileID, err := uuid.Parse(c.Param("fileID"))
if err != nil {
logger.Info("could not parse query parameter `fileID` as UUID", zap.Error(err))
return service.NewBadInputError("invalid file ID").
WithInternal(err)
}
if err := h.filterAccountManagerOrFileCreator(ctx, &loginUser, fileID); err != nil {
return err
}
err = h.Repository.DeleteFile(ctx, fileID)
if err != nil {
logger.Error("failed to delete file in repository",
zap.String("ID", fileID.String()), zap.Error(err))
return service.NewUnexpectedError(err)
}
err = h.Storage.Delete(ctx, fileID.String())
if err != nil {
logger.Error(
"failed to delete file in storage",
zap.String("ID", fileID.String()), zap.Error(err))
return service.NewUnexpectedError(err)
}
return c.NoContent(http.StatusOK)
}
// isFileCreator 与えられたユーザーがファイルの作成者かどうかを確認します
func (h Handlers) isFileCreator(ctx context.Context, userID, fileID uuid.UUID) (bool, error) {
file, err := h.Repository.GetFile(ctx, fileID)
if err != nil {
return false, err
}
return file.CreatedBy == userID, nil
}
func (h Handlers) filterAccountManagerOrFileCreator(
ctx context.Context, user *User, fileID uuid.UUID,
) error {
logger := logging.GetLogger(ctx)
if user.AccountManager {
return nil
}
isCreator, err := h.isFileCreator(ctx, user.ID, fileID)
if err != nil {
logger.Error("failed to check if user is file creator", zap.Error(err))
return echo.ErrInternalServerError.Wrap(err)
}
if isCreator {
return nil
}
return echo.ErrForbidden.Wrap(errUserIsNotAccountManagerOrFileCreator)
}