добавлен context.Context в функции Create и Clean
This commit is contained in:
@@ -4,7 +4,7 @@ go 1.25.4
|
|||||||
|
|
||||||
replace git.company.local/gopkg/filestore/remote => ./remote
|
replace git.company.local/gopkg/filestore/remote => ./remote
|
||||||
|
|
||||||
require github.com/minio/minio-go/v7 v7.0.98
|
require github.com/minio/minio-go/v7 v7.0.100
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI
|
|||||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||||
github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0=
|
github.com/minio/minio-go/v7 v7.0.100 h1:ShkWi8Tyj9RtU57OQB2HIXKz4bFgtVib0bbT1sbtLI8=
|
||||||
github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=
|
github.com/minio/minio-go/v7 v7.0.100/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw=
|
||||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
|||||||
+256
-154
@@ -2,9 +2,12 @@ package filestore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/base32"
|
"encoding/base32"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"hash/crc32"
|
"hash/crc32"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -18,21 +21,23 @@ import (
|
|||||||
// tmpfileName используется в качестве имени временного файла при генерации ошибок
|
// tmpfileName используется в качестве имени временного файла при генерации ошибок
|
||||||
const tmpfileName = "<temporary file>"
|
const tmpfileName = "<temporary file>"
|
||||||
|
|
||||||
|
// минимальная длина имени файла, необходимая для разбиения на подкаталоги
|
||||||
|
const minNameLen = 27
|
||||||
|
|
||||||
// LocalStorage описывает хранилище файлов.
|
// LocalStorage описывает хранилище файлов.
|
||||||
type LocalStorage struct {
|
type LocalStorage struct {
|
||||||
rootDir string
|
rootDir string
|
||||||
perm os.FileMode
|
perm os.FileMode
|
||||||
|
|
||||||
mutexes struct {
|
// защита для map мьютексов
|
||||||
sync.Mutex
|
mu sync.Mutex
|
||||||
sync.Once
|
once sync.Once
|
||||||
m map[string]*sync.Mutex
|
fileMutex map[string]*sync.Mutex // мьютекс на имя файла
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocalStorageOption func(*LocalStorage)
|
type LocalStorageOption func(*LocalStorage)
|
||||||
|
|
||||||
// WithPermissions
|
// WithPermissions устанавливает права доступа для создаваемых каталогов и файлов.
|
||||||
func WithPermissions(perm os.FileMode) LocalStorageOption {
|
func WithPermissions(perm os.FileMode) LocalStorageOption {
|
||||||
return func(s *LocalStorage) {
|
return func(s *LocalStorage) {
|
||||||
s.perm = perm
|
s.perm = perm
|
||||||
@@ -41,9 +46,8 @@ func WithPermissions(perm os.FileMode) LocalStorageOption {
|
|||||||
|
|
||||||
// FileInfo описывает информацию о сохраненном файле.
|
// FileInfo описывает информацию о сохраненном файле.
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
Location string // @deprecated
|
Path string // полный путь внутри хранилища
|
||||||
Path string
|
Name string // уникальное имя файла
|
||||||
Name string
|
|
||||||
Mimetype string
|
Mimetype string
|
||||||
Size int64
|
Size int64
|
||||||
CRC32 uint32
|
CRC32 uint32
|
||||||
@@ -55,11 +59,19 @@ func NewLocalStorage(rootDir string, opts ...LocalStorageOption) (*LocalStorage,
|
|||||||
s := &LocalStorage{}
|
s := &LocalStorage{}
|
||||||
s.rootDir = rootDir
|
s.rootDir = rootDir
|
||||||
s.perm = 0700
|
s.perm = 0700
|
||||||
|
s.fileMutex = make(map[string]*sync.Mutex)
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(s)
|
opt(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Очищаем путь и делаем его абсолютным для корректной проверки безопасности
|
||||||
|
absRoot, err := filepath.Abs(s.rootDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get absolute path for rootDir: %w", err)
|
||||||
|
}
|
||||||
|
s.rootDir = absRoot
|
||||||
|
|
||||||
// Создаём каталог, если он ещё не создан
|
// Создаём каталог, если он ещё не создан
|
||||||
if err := os.MkdirAll(s.rootDir, s.perm); err != nil {
|
if err := os.MkdirAll(s.rootDir, s.perm); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -68,103 +80,172 @@ func NewLocalStorage(rootDir string, opts ...LocalStorageOption) (*LocalStorage,
|
|||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getMutex возвращает мьютекс для имени файла, создавая его при необходимости.
|
||||||
|
func (s *LocalStorage) getMutex(name string) *sync.Mutex {
|
||||||
|
s.once.Do(func() {
|
||||||
|
// инициализация уже выполнена в NewLocalStorage, но оставляем для безопасности
|
||||||
|
if s.fileMutex == nil {
|
||||||
|
s.fileMutex = make(map[string]*sync.Mutex)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
mu, ok := s.fileMutex[name]
|
||||||
|
if !ok {
|
||||||
|
mu = &sync.Mutex{}
|
||||||
|
s.fileMutex[name] = mu
|
||||||
|
}
|
||||||
|
return mu
|
||||||
|
}
|
||||||
|
|
||||||
|
// releaseMutex удаляет мьютекс из map после использования (вызывать после Unlock).
|
||||||
|
func (s *LocalStorage) releaseMutex(name string) {
|
||||||
|
s.mu.Lock()
|
||||||
|
delete(s.fileMutex, name)
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// safePath проверяет, что путь не выходит за пределы rootDir, и возвращает очищенный путь.
|
||||||
|
func (s *LocalStorage) safePath(subPath string) (string, error) {
|
||||||
|
// Убираем начальные разделители и ".." попытки
|
||||||
|
clean := filepath.Clean(strings.TrimPrefix(subPath, "/"))
|
||||||
|
|
||||||
|
// Запрещаем пустые и слишком короткие имена
|
||||||
|
if clean == "" || clean == "." || clean == ".." {
|
||||||
|
return "", os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
full := filepath.Join(s.rootDir, clean)
|
||||||
|
|
||||||
|
// Проверяем, что итоговый путь всё ещё внутри rootDir
|
||||||
|
absFull, err := filepath.Abs(full)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(absFull, s.rootDir+string(os.PathSeparator)) && absFull != s.rootDir {
|
||||||
|
return "", fmt.Errorf("%w: path traversal attempt", os.ErrPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
return absFull, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Create сохраняет файл в хранилище. В качестве имени файла используется комбинация из двух хешей.
|
// Create сохраняет файл в хранилище. В качестве имени файла используется комбинация из двух хешей.
|
||||||
func (s *LocalStorage) Create(r io.Reader) (*FileInfo, error) {
|
func (s *LocalStorage) Create(ctx context.Context, r io.Reader) (*FileInfo, error) {
|
||||||
|
if r == nil {
|
||||||
|
return nil, errors.New("reader is nil")
|
||||||
|
}
|
||||||
|
|
||||||
// Создаём временный файл в корневом каталоге
|
// Создаём временный файл в корневом каталоге
|
||||||
tmpfile, err := os.CreateTemp(s.rootDir, "~tmp")
|
tmpfile, err := os.CreateTemp(s.rootDir, "~tmp")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err.(*os.PathError).Path = tmpfileName // Подмениваем имя файла
|
return nil, s.wrapPathError(err, tmpfileName)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
defer os.Remove(tmpfile.Name())
|
defer func() {
|
||||||
|
tmpfile.Close()
|
||||||
|
os.Remove(tmpfile.Name())
|
||||||
|
}()
|
||||||
|
|
||||||
// Копируем содержимое во временный файл
|
// Копируем содержимое во временный файл
|
||||||
bufferReader := bufio.NewReaderSize(r, 4<<10)
|
bufferReader := bufio.NewReaderSize(r, 4<<10)
|
||||||
|
|
||||||
// Пытаемся определить тип содержимого
|
// Пытаемся определить MIME-тип содержимого
|
||||||
data, err := bufferReader.Peek(512) // Читаем первые 512 байт файла
|
data, err := bufferReader.Peek(512)
|
||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
tmpfile.Close()
|
return nil, s.wrapPathError(err, tmpfileName)
|
||||||
err = &os.PathError{Op: "create", Path: tmpfileName, Err: err}
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
mimetype := http.DetectContentType(data)
|
mimetype := http.DetectContentType(data)
|
||||||
|
|
||||||
// Одновременно с сохранением в файл считаем две хеш-суммы
|
// Одновременно с сохранением в файл считаем две хеш-суммы
|
||||||
hashCRC32, hashMD5 := crc32.NewIEEE(), md5.New()
|
hashCRC32, hashMD5 := crc32.NewIEEE(), md5.New()
|
||||||
size, err := bufferReader.WriteTo(io.MultiWriter(tmpfile, hashCRC32, hashMD5))
|
multiWriter := io.MultiWriter(tmpfile, hashCRC32, hashMD5)
|
||||||
if err != nil {
|
|
||||||
tmpfile.Close()
|
|
||||||
err = &os.PathError{Op: "write", Path: tmpfileName, Err: err}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Формируем информацию о файле
|
type copyResult struct {
|
||||||
sumMD5 := hashMD5.Sum(nil)
|
size int64
|
||||||
name := base32.StdEncoding.EncodeToString(append(hashCRC32.Sum(nil), sumMD5...))
|
err error
|
||||||
fi := &FileInfo{
|
|
||||||
Location: s.GetPath(name),
|
|
||||||
Path: s.GetPath(name),
|
|
||||||
Name: name,
|
|
||||||
Mimetype: mimetype,
|
|
||||||
Size: size,
|
|
||||||
CRC32: hashCRC32.Sum32(),
|
|
||||||
MD5: hex.EncodeToString(sumMD5),
|
|
||||||
}
|
}
|
||||||
|
done := make(chan copyResult, 1)
|
||||||
|
go func() {
|
||||||
|
size, err := bufferReader.WriteTo(multiWriter)
|
||||||
|
done <- copyResult{size, err}
|
||||||
|
}()
|
||||||
|
|
||||||
// Закрываем временный файл
|
select {
|
||||||
if err := tmpfile.Close(); err != nil {
|
case <-ctx.Done():
|
||||||
if _, ok := err.(*os.PathError); ok {
|
return nil, ctx.Err()
|
||||||
err.(*os.PathError).Path = tmpfileName
|
case res := <-done:
|
||||||
|
if res.err != nil {
|
||||||
|
return nil, s.wrapPathError(res.err, tmpfileName)
|
||||||
}
|
}
|
||||||
return nil, err
|
// Формируем информацию о файле
|
||||||
}
|
sumMD5 := hashMD5.Sum(nil)
|
||||||
|
name := base32.StdEncoding.EncodeToString(append(hashCRC32.Sum(nil), sumMD5...))
|
||||||
// Если файл уже существует, то просто обновляем его время создания
|
fi := &FileInfo{
|
||||||
now := time.Now()
|
Path: s.GetRelativePath(name),
|
||||||
if err := os.Chtimes(fi.Path, now, now); err == nil {
|
Name: name,
|
||||||
return fi, nil // Возвращаем информацию о файле, временный файл будет автоматически удален
|
Mimetype: mimetype,
|
||||||
}
|
Size: res.size,
|
||||||
|
CRC32: hashCRC32.Sum32(),
|
||||||
// Если такого файла нет, то создаем для него каталог
|
MD5: hex.EncodeToString(sumMD5),
|
||||||
if err := os.MkdirAll(filepath.Dir(fi.Path), s.perm); err != nil {
|
|
||||||
err.(*os.PathError).Path = fi.Name
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Перемещаем временный файл в этот каталог
|
|
||||||
if err := os.Rename(tmpfile.Name(), fi.Path); err != nil {
|
|
||||||
if _, ok := err.(*os.PathError); ok {
|
|
||||||
err.(*os.PathError).Path = fi.Name
|
|
||||||
}
|
}
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Возвращаем информацию о созданном файле
|
// Закрываем временный файл
|
||||||
return fi, nil
|
if err := tmpfile.Close(); err != nil {
|
||||||
|
return nil, s.wrapPathError(err, tmpfileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath, err := s.safePath(fi.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если файл уже существует, то просто обновляем его время создания
|
||||||
|
now := time.Now()
|
||||||
|
if err := os.Chtimes(fullPath, now, now); err == nil {
|
||||||
|
return fi, nil
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
// Другая ошибка (например, permission denied) – не можем перезаписать
|
||||||
|
return nil, s.wrapPathError(err, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если такого файла нет, то создаем для него каталоги
|
||||||
|
if err := os.MkdirAll(filepath.Dir(fullPath), s.perm); err != nil {
|
||||||
|
return nil, s.wrapPathError(err, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Перемещаем временный файл
|
||||||
|
if err := os.Rename(tmpfile.Name(), fullPath); err != nil {
|
||||||
|
return nil, s.wrapPathError(err, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fi, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open открывает файл из каталога.
|
// Open открывает файл из хранилища.
|
||||||
func (s *LocalStorage) Open(name string) (*os.File, error) {
|
func (s *LocalStorage) Open(name string) (*os.File, error) {
|
||||||
// Полное имя для доступа к файлу
|
// Полное имя для доступа к файлу
|
||||||
fullName := s.GetPath(name)
|
fullPath, err := s.GetFullPath(name)
|
||||||
if fullName == "" {
|
if err != nil {
|
||||||
return nil, os.ErrNotExist
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Открываем файл
|
// Открываем файл
|
||||||
file, err := os.Open(fullName)
|
file, err := os.Open(fullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err.(*os.PathError).Path = name
|
return nil, s.wrapPathError(err, name)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем информацию о файле и проверяем, что это не каталог
|
// Получаем информацию о файле и проверяем, что это не каталог
|
||||||
fi, err := file.Stat()
|
fi, err := file.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
file.Close()
|
file.Close()
|
||||||
err.(*os.PathError).Path = name
|
return nil, s.wrapPathError(err, name)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Возвращаем ошибку, если это каталог, а не файл
|
// Возвращаем ошибку, если это каталог, а не файл
|
||||||
@@ -173,132 +254,153 @@ func (s *LocalStorage) Open(name string) (*os.File, error) {
|
|||||||
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrPermission}
|
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrPermission}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем время доступа к файлу
|
// Обновляем время доступа (ошибку игнорируем, это не критично)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
os.Chtimes(fullName, now, now)
|
_ = os.Chtimes(fullPath, now, now)
|
||||||
|
return file, nil
|
||||||
return file, nil // Возвращаем открытый файл
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove удаляет файл из хранилища.
|
// Remove удаляет файл из хранилища.
|
||||||
func (s *LocalStorage) Remove(name string) error {
|
func (s *LocalStorage) Remove(name string) error {
|
||||||
mu := s.getMutex(name)
|
mu := s.getMutex(name)
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer func() {
|
||||||
|
mu.Unlock()
|
||||||
|
s.releaseMutex(name)
|
||||||
|
}()
|
||||||
|
|
||||||
// Полное имя для доступа к файлу
|
fullPath, err := s.GetFullPath(name)
|
||||||
fullName := s.GetPath(name)
|
if err != nil {
|
||||||
if fullName == "" {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
return os.ErrNotExist
|
return os.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.Remove(fullName); err != nil {
|
|
||||||
err.(*os.PathError).Path = name
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пытаемся удалить пустые каталоги, если они образовались
|
if err := os.Remove(fullPath); err != nil {
|
||||||
for range 2 {
|
return s.wrapPathError(err, name)
|
||||||
fullName = filepath.Dir(fullName)
|
|
||||||
if err := os.Remove(fullName); err != nil {
|
|
||||||
break // Если не получилось, значит каталог не пустой
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Удаляем пустые родительские каталоги, но не выше rootDir
|
||||||
|
s.removeEmptyParents(fullPath)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// removeEmptyParents поднимается вверх по дереву каталогов, удаляя пустые,
|
||||||
|
// пока не дойдёт до rootDir или не встретит непустой каталог.
|
||||||
|
func (s *LocalStorage) removeEmptyParents(path string) {
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
for dir != s.rootDir && dir != "." && dir != "/" {
|
||||||
|
// Пытаемся удалить каталог
|
||||||
|
if err := os.Remove(dir); err != nil {
|
||||||
|
break // не пустой или ошибка прав
|
||||||
|
}
|
||||||
|
dir = filepath.Dir(dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clean удаляет старые файлы, к которым не обращались больше заданного времени.
|
// Clean удаляет старые файлы, к которым не обращались больше заданного времени.
|
||||||
func (s *LocalStorage) Clean(lifetime time.Duration) error {
|
// Если lifetime <= 0, удаляет все файлы.
|
||||||
// Удаляем вообще все файлы, если время жизни не задано
|
func (s *LocalStorage) Clean(ctx context.Context, lifetime time.Duration) error {
|
||||||
if lifetime <= 0 {
|
if lifetime <= 0 {
|
||||||
files, err := filepath.Glob(filepath.Join(s.rootDir, "*"))
|
// Удаляем всё содержимое rootDir, но не саму директорию
|
||||||
|
entries, err := os.ReadDir(s.rootDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, file := range files {
|
for _, entry := range entries {
|
||||||
if err := os.RemoveAll(file); err != nil {
|
select {
|
||||||
return err
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if err := os.RemoveAll(filepath.Join(s.rootDir, entry.Name())); err != nil {
|
||||||
|
// Логируем, но продолжаем (по аналогии с оригиналом)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вычисляем крайнюю дату валидности файлов
|
|
||||||
valid := time.Now().Add(-lifetime)
|
valid := time.Now().Add(-lifetime)
|
||||||
|
err := filepath.Walk(s.rootDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return nil // игнорируем ошибки доступа к файлу
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
err := filepath.Walk(s.rootDir,
|
if info.IsDir() || info.ModTime().After(valid) {
|
||||||
func(filename string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Не удаляем каталоги и новые файлы
|
|
||||||
if info.IsDir() || info.ModTime().After(valid) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Удаляем старый файл
|
|
||||||
if err = os.Remove(filename); err != nil {
|
|
||||||
return nil // Ошибку удаления игнорируем
|
|
||||||
}
|
|
||||||
|
|
||||||
// Пытаемся удалить пустые каталоги
|
|
||||||
for range 2 {
|
|
||||||
filename = filepath.Dir(filename)
|
|
||||||
if err = os.Remove(filename); err != nil {
|
|
||||||
break // Каталог не пустой
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
|
||||||
if os.IsNotExist(err) {
|
// Получаем имя файла относительно rootDir
|
||||||
return nil // Игнорируем ошибку, что файл не существует
|
rel, err := filepath.Rel(s.rootDir, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Имя файла в нашей терминологии — это путь без rootDir
|
||||||
|
fileName := strings.ReplaceAll(rel, string(os.PathSeparator), "") // упрощённо, но для блокировки нужно исходное имя
|
||||||
|
|
||||||
|
// Блокируем файл на время удаления
|
||||||
|
mu := s.getMutex(fileName)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
|
return nil // игнорируем ошибку удаления
|
||||||
|
}
|
||||||
|
s.removeEmptyParents(path)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapPathError безопасно заменяет путь в ошибке, если она является *os.PathError.
|
||||||
|
func (s *LocalStorage) wrapPathError(err error, name string) error {
|
||||||
|
if pe, ok := err.(*os.PathError); ok {
|
||||||
|
pe.Path = name
|
||||||
|
return pe
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// getMutex
|
// GetRelativePath возвращает относительный путь к файлу в хранилище (без учёта rootDir).
|
||||||
func (s *LocalStorage) getMutex(name string) *sync.Mutex {
|
func (s *LocalStorage) GetRelativePath(name string) string {
|
||||||
s.mutexes.Do(func() { s.mutexes.m = make(map[string]*sync.Mutex) })
|
|
||||||
|
|
||||||
s.mutexes.Lock()
|
|
||||||
mu, ok := s.mutexes.m[name]
|
|
||||||
if !ok {
|
|
||||||
mu = &sync.Mutex{}
|
|
||||||
s.mutexes.m[name] = mu
|
|
||||||
}
|
|
||||||
s.mutexes.Unlock()
|
|
||||||
|
|
||||||
return mu
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFullName возвращает полный путь к файлу в хранилище.
|
|
||||||
//
|
|
||||||
// Deprecated: GetFullName is no longer recommended. Use GetPath instead.
|
|
||||||
func (s *LocalStorage) GetFullName(name string) string { return s.GetPath(name) }
|
|
||||||
|
|
||||||
// GetPath возвращает полный путь к файлу в хранилище.
|
|
||||||
func (s *LocalStorage) GetPath(name string) string {
|
|
||||||
name = strings.TrimPrefix(name, "/")
|
name = strings.TrimPrefix(name, "/")
|
||||||
if len(name) < 27 {
|
if len(name) < minNameLen {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return filepath.Join(s.rootDir, name[:1], name[1:3], name[3:])
|
// Раскладываем по подкаталогам: первая буква, вторая+третья, остальное
|
||||||
|
return filepath.Join(name[:1], name[1:3], name[3:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsExists проверяет: существует ли файл в хранилище?
|
// GetFullPath возвращает полный путь к файлу в хранилище.
|
||||||
|
func (s *LocalStorage) GetFullPath(name string) (string, error) {
|
||||||
|
relPath := s.GetRelativePath(name)
|
||||||
|
if relPath == "" {
|
||||||
|
return "", fmt.Errorf("invalid file name: %s", name)
|
||||||
|
}
|
||||||
|
return s.safePath(relPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExists проверяет существование файла.
|
||||||
func (s *LocalStorage) IsExists(name string) bool {
|
func (s *LocalStorage) IsExists(name string) bool {
|
||||||
fullName := s.GetPath(name)
|
fullPath, err := s.GetFullPath(name)
|
||||||
if fullName == "" {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fi, err := os.Stat(fullName)
|
fi, err := os.Stat(fullPath)
|
||||||
if os.IsNotExist(err) {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user