Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
alist-org
GitHub Repository: alist-org/alist
Path: blob/main/drivers/local/driver.go
1986 views
1
package local
2
3
import (
4
"bytes"
5
"context"
6
"errors"
7
"fmt"
8
"io/fs"
9
"net/http"
10
"os"
11
stdpath "path"
12
"path/filepath"
13
"strconv"
14
"strings"
15
"time"
16
17
"github.com/alist-org/alist/v3/internal/conf"
18
"github.com/alist-org/alist/v3/internal/driver"
19
"github.com/alist-org/alist/v3/internal/errs"
20
"github.com/alist-org/alist/v3/internal/model"
21
"github.com/alist-org/alist/v3/internal/sign"
22
"github.com/alist-org/alist/v3/pkg/utils"
23
"github.com/alist-org/alist/v3/server/common"
24
"github.com/alist-org/times"
25
cp "github.com/otiai10/copy"
26
log "github.com/sirupsen/logrus"
27
_ "golang.org/x/image/webp"
28
)
29
30
type Local struct {
31
model.Storage
32
Addition
33
mkdirPerm int32
34
35
// zero means no limit
36
thumbConcurrency int
37
thumbTokenBucket TokenBucket
38
39
// video thumb position
40
videoThumbPos float64
41
videoThumbPosIsPercentage bool
42
}
43
44
func (d *Local) Config() driver.Config {
45
return config
46
}
47
48
func (d *Local) Init(ctx context.Context) error {
49
if d.MkdirPerm == "" {
50
d.mkdirPerm = 0777
51
} else {
52
v, err := strconv.ParseUint(d.MkdirPerm, 8, 32)
53
if err != nil {
54
return err
55
}
56
d.mkdirPerm = int32(v)
57
}
58
if !utils.Exists(d.GetRootPath()) {
59
return fmt.Errorf("root folder %s not exists", d.GetRootPath())
60
}
61
if !filepath.IsAbs(d.GetRootPath()) {
62
abs, err := filepath.Abs(d.GetRootPath())
63
if err != nil {
64
return err
65
}
66
d.Addition.RootFolderPath = abs
67
}
68
if d.ThumbCacheFolder != "" && !utils.Exists(d.ThumbCacheFolder) {
69
err := os.MkdirAll(d.ThumbCacheFolder, os.FileMode(d.mkdirPerm))
70
if err != nil {
71
return err
72
}
73
}
74
if d.ThumbConcurrency != "" {
75
v, err := strconv.ParseUint(d.ThumbConcurrency, 10, 32)
76
if err != nil {
77
return err
78
}
79
d.thumbConcurrency = int(v)
80
}
81
if d.thumbConcurrency == 0 {
82
d.thumbTokenBucket = NewNopTokenBucket()
83
} else {
84
d.thumbTokenBucket = NewStaticTokenBucketWithMigration(d.thumbTokenBucket, d.thumbConcurrency)
85
}
86
// Check the VideoThumbPos value
87
if d.VideoThumbPos == "" {
88
d.VideoThumbPos = "20%"
89
}
90
if strings.HasSuffix(d.VideoThumbPos, "%") {
91
percentage := strings.TrimSuffix(d.VideoThumbPos, "%")
92
val, err := strconv.ParseFloat(percentage, 64)
93
if err != nil {
94
return fmt.Errorf("invalid video_thumb_pos value: %s, err: %s", d.VideoThumbPos, err)
95
}
96
if val < 0 || val > 100 {
97
return fmt.Errorf("invalid video_thumb_pos value: %s, the precentage must be a number between 0 and 100", d.VideoThumbPos)
98
}
99
d.videoThumbPosIsPercentage = true
100
d.videoThumbPos = val / 100
101
} else {
102
val, err := strconv.ParseFloat(d.VideoThumbPos, 64)
103
if err != nil {
104
return fmt.Errorf("invalid video_thumb_pos value: %s, err: %s", d.VideoThumbPos, err)
105
}
106
if val < 0 {
107
return fmt.Errorf("invalid video_thumb_pos value: %s, the time must be a positive number", d.VideoThumbPos)
108
}
109
d.videoThumbPosIsPercentage = false
110
d.videoThumbPos = val
111
}
112
return nil
113
}
114
115
func (d *Local) Drop(ctx context.Context) error {
116
return nil
117
}
118
119
func (d *Local) GetAddition() driver.Additional {
120
return &d.Addition
121
}
122
123
func (d *Local) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
124
fullPath := dir.GetPath()
125
rawFiles, err := readDir(fullPath)
126
if err != nil {
127
return nil, err
128
}
129
var files []model.Obj
130
for _, f := range rawFiles {
131
if !d.ShowHidden && strings.HasPrefix(f.Name(), ".") {
132
continue
133
}
134
file := d.FileInfoToObj(ctx, f, args.ReqPath, fullPath)
135
files = append(files, file)
136
}
137
return files, nil
138
}
139
func (d *Local) FileInfoToObj(ctx context.Context, f fs.FileInfo, reqPath string, fullPath string) model.Obj {
140
thumb := ""
141
if d.Thumbnail {
142
typeName := utils.GetFileType(f.Name())
143
if typeName == conf.IMAGE || typeName == conf.VIDEO {
144
thumb = common.GetApiUrl(common.GetHttpReq(ctx)) + stdpath.Join("/d", reqPath, f.Name())
145
thumb = utils.EncodePath(thumb, true)
146
thumb += "?type=thumb&sign=" + sign.Sign(stdpath.Join(reqPath, f.Name()))
147
}
148
}
149
filePath := filepath.Join(fullPath, f.Name())
150
isFolder := f.IsDir() || isLinkedDir(f, filePath)
151
var size int64
152
if !isFolder {
153
size = f.Size()
154
}
155
var ctime time.Time
156
t, err := times.Stat(filePath)
157
if err == nil {
158
if t.HasBirthTime() {
159
ctime = t.BirthTime()
160
}
161
}
162
163
file := model.ObjThumb{
164
Object: model.Object{
165
Path: filePath,
166
Name: f.Name(),
167
Modified: f.ModTime(),
168
Size: size,
169
IsFolder: isFolder,
170
Ctime: ctime,
171
},
172
Thumbnail: model.Thumbnail{
173
Thumbnail: thumb,
174
},
175
}
176
return &file
177
}
178
func (d *Local) GetMeta(ctx context.Context, path string) (model.Obj, error) {
179
f, err := os.Stat(path)
180
if err != nil {
181
return nil, err
182
}
183
file := d.FileInfoToObj(ctx, f, path, path)
184
//h := "123123"
185
//if s, ok := f.(model.SetHash); ok && file.GetHash() == ("","") {
186
// s.SetHash(h,"SHA1")
187
//}
188
return file, nil
189
190
}
191
192
func (d *Local) Get(ctx context.Context, path string) (model.Obj, error) {
193
path = filepath.Join(d.GetRootPath(), path)
194
f, err := os.Stat(path)
195
if err != nil {
196
if strings.Contains(err.Error(), "cannot find the file") {
197
return nil, errs.ObjectNotFound
198
}
199
return nil, err
200
}
201
isFolder := f.IsDir() || isLinkedDir(f, path)
202
size := f.Size()
203
if isFolder {
204
size = 0
205
}
206
var ctime time.Time
207
t, err := times.Stat(path)
208
if err == nil {
209
if t.HasBirthTime() {
210
ctime = t.BirthTime()
211
}
212
}
213
file := model.Object{
214
Path: path,
215
Name: f.Name(),
216
Modified: f.ModTime(),
217
Ctime: ctime,
218
Size: size,
219
IsFolder: isFolder,
220
}
221
return &file, nil
222
}
223
224
func (d *Local) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
225
fullPath := file.GetPath()
226
var link model.Link
227
if args.Type == "thumb" && utils.Ext(file.GetName()) != "svg" {
228
var buf *bytes.Buffer
229
var thumbPath *string
230
err := d.thumbTokenBucket.Do(ctx, func() error {
231
var err error
232
buf, thumbPath, err = d.getThumb(file)
233
return err
234
})
235
if err != nil {
236
return nil, err
237
}
238
link.Header = http.Header{
239
"Content-Type": []string{"image/png"},
240
}
241
if thumbPath != nil {
242
open, err := os.Open(*thumbPath)
243
if err != nil {
244
return nil, err
245
}
246
link.MFile = open
247
} else {
248
link.MFile = model.NewNopMFile(bytes.NewReader(buf.Bytes()))
249
//link.Header.Set("Content-Length", strconv.Itoa(buf.Len()))
250
}
251
} else {
252
open, err := os.Open(fullPath)
253
if err != nil {
254
return nil, err
255
}
256
link.MFile = open
257
}
258
return &link, nil
259
}
260
261
func (d *Local) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
262
fullPath := filepath.Join(parentDir.GetPath(), dirName)
263
err := os.MkdirAll(fullPath, os.FileMode(d.mkdirPerm))
264
if err != nil {
265
return err
266
}
267
return nil
268
}
269
270
func (d *Local) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
271
srcPath := srcObj.GetPath()
272
dstPath := filepath.Join(dstDir.GetPath(), srcObj.GetName())
273
if utils.IsSubPath(srcPath, dstPath) {
274
return fmt.Errorf("the destination folder is a subfolder of the source folder")
275
}
276
if err := os.Rename(srcPath, dstPath); err != nil && strings.Contains(err.Error(), "invalid cross-device link") {
277
// Handle cross-device file move in local driver
278
if err = d.Copy(ctx, srcObj, dstDir); err != nil {
279
return err
280
} else {
281
// Directly remove file without check recycle bin if successfully copied
282
if srcObj.IsDir() {
283
err = os.RemoveAll(srcObj.GetPath())
284
} else {
285
err = os.Remove(srcObj.GetPath())
286
}
287
return err
288
}
289
} else {
290
return err
291
}
292
}
293
294
func (d *Local) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
295
srcPath := srcObj.GetPath()
296
dstPath := filepath.Join(filepath.Dir(srcPath), newName)
297
err := os.Rename(srcPath, dstPath)
298
if err != nil {
299
return err
300
}
301
return nil
302
}
303
304
func (d *Local) Copy(_ context.Context, srcObj, dstDir model.Obj) error {
305
srcPath := srcObj.GetPath()
306
dstPath := filepath.Join(dstDir.GetPath(), srcObj.GetName())
307
if utils.IsSubPath(srcPath, dstPath) {
308
return fmt.Errorf("the destination folder is a subfolder of the source folder")
309
}
310
// Copy using otiai10/copy to perform more secure & efficient copy
311
return cp.Copy(srcPath, dstPath, cp.Options{
312
Sync: true, // Sync file to disk after copy, may have performance penalty in filesystem such as ZFS
313
PreserveTimes: true,
314
PreserveOwner: true,
315
})
316
}
317
318
func (d *Local) Remove(ctx context.Context, obj model.Obj) error {
319
var err error
320
if utils.SliceContains([]string{"", "delete permanently"}, d.RecycleBinPath) {
321
if obj.IsDir() {
322
err = os.RemoveAll(obj.GetPath())
323
} else {
324
err = os.Remove(obj.GetPath())
325
}
326
} else {
327
dstPath := filepath.Join(d.RecycleBinPath, obj.GetName())
328
if utils.Exists(dstPath) {
329
dstPath = filepath.Join(d.RecycleBinPath, obj.GetName()+"_"+time.Now().Format("20060102150405"))
330
}
331
err = os.Rename(obj.GetPath(), dstPath)
332
}
333
if err != nil {
334
return err
335
}
336
return nil
337
}
338
339
func (d *Local) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
340
fullPath := filepath.Join(dstDir.GetPath(), stream.GetName())
341
out, err := os.Create(fullPath)
342
if err != nil {
343
return err
344
}
345
defer func() {
346
_ = out.Close()
347
if errors.Is(err, context.Canceled) {
348
_ = os.Remove(fullPath)
349
}
350
}()
351
err = utils.CopyWithCtx(ctx, out, stream, stream.GetSize(), up)
352
if err != nil {
353
return err
354
}
355
err = os.Chtimes(fullPath, stream.ModTime(), stream.ModTime())
356
if err != nil {
357
log.Errorf("[local] failed to change time of %s: %s", fullPath, err)
358
}
359
return nil
360
}
361
362
var _ driver.Driver = (*Local)(nil)
363
364