302 lines
5.4 KiB
Go
302 lines
5.4 KiB
Go
package pfs_db
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.gm6.ru/icewind/pfs/common"
|
|
"git.gm6.ru/icewind/pfs/models"
|
|
"git.gm6.ru/icewind/pfs/stat"
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type PFSDB struct {
|
|
DB *gorm.DB
|
|
}
|
|
|
|
//
|
|
// ==================== DIR ====================
|
|
//
|
|
|
|
func (i *PFSDB) DirCreate(dir *models.Dir, userID uuid.UUID) error {
|
|
now := time.Now()
|
|
|
|
// Проверяем ownership родителя (если есть)
|
|
if dir.ParentID != nil {
|
|
var parent DirModel
|
|
err := i.DB.First(
|
|
&parent,
|
|
"id = ? AND owner_id = ?",
|
|
*dir.ParentID,
|
|
userID,
|
|
).Error
|
|
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return common.ErrAccessDenied
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
dbDir := DirModel{
|
|
ID: dir.ID,
|
|
ParentID: dir.ParentID,
|
|
Name: dir.Name,
|
|
OwnerID: dir.OwnerID,
|
|
IsPublic: dir.IsPublic,
|
|
Created: now,
|
|
LastModified: now,
|
|
}
|
|
|
|
return i.DB.Create(&dbDir).Error
|
|
}
|
|
|
|
func (i *PFSDB) DirRemove(dirID uuid.UUID, userID uuid.UUID) error {
|
|
var dir DirModel
|
|
|
|
if err := i.DB.First(
|
|
&dir,
|
|
"id = ? AND owner_id = ?",
|
|
dirID,
|
|
userID,
|
|
).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// Cascade делает БД
|
|
return i.DB.Delete(&dir).Error
|
|
}
|
|
|
|
func (i *PFSDB) DirRead(dirID uuid.UUID, userID uuid.UUID) ([]stat.FSStat, error) {
|
|
var result []stat.FSStat
|
|
|
|
// Поддиректории
|
|
var dirs []DirModel
|
|
if err := i.DB.
|
|
Where("parent_id = ? AND owner_id = ?", dirID, userID).
|
|
Order("name").
|
|
Find(&dirs).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, d := range dirs {
|
|
result = append(result, stat.FSStat{
|
|
ID: d.ID,
|
|
Name: d.Name,
|
|
IsDir: true,
|
|
Created: d.Created,
|
|
LastModified: d.LastModified,
|
|
OwnerID: d.OwnerID,
|
|
})
|
|
}
|
|
|
|
// Файлы
|
|
var files []FileModel
|
|
if err := i.DB.
|
|
Where("dir_id = ? AND owner_id = ?", dirID, userID).
|
|
Order("name").
|
|
Find(&files).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, f := range files {
|
|
result = append(result, stat.FSStat{
|
|
ID: f.ID,
|
|
Name: f.Name,
|
|
IsDir: false,
|
|
Created: f.Created,
|
|
LastModified: f.LastModified,
|
|
OwnerID: f.OwnerID,
|
|
})
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
//
|
|
// ==================== FILE ====================
|
|
//
|
|
|
|
func (i *PFSDB) FileCreate(file *models.File) error {
|
|
file.ID = uuid.New()
|
|
now := time.Now()
|
|
|
|
return i.DB.Transaction(func(tx *gorm.DB) error {
|
|
dbFile := FileModel{
|
|
ID: file.ID,
|
|
DirID: file.DirID,
|
|
Name: file.Name,
|
|
OwnerID: file.OwnerID,
|
|
IsPublic: file.IsPublic,
|
|
Created: now,
|
|
LastModified: now,
|
|
}
|
|
|
|
if err := tx.Create(&dbFile).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
dbFileData := FileDataModel{
|
|
ID: uuid.New(),
|
|
FileID: file.ID,
|
|
Data: file.Data,
|
|
}
|
|
|
|
return tx.Create(&dbFileData).Error
|
|
})
|
|
}
|
|
|
|
func (i *PFSDB) FileRemove(fileID uuid.UUID, userID uuid.UUID) error {
|
|
var file FileModel
|
|
|
|
if err := i.DB.First(
|
|
&file,
|
|
"id = ? AND owner_id = ?",
|
|
fileID,
|
|
userID,
|
|
).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// FileData удалится через CASCADE
|
|
return i.DB.Delete(&file).Error
|
|
}
|
|
|
|
func (i *PFSDB) FileRead(fileID uuid.UUID, userID uuid.UUID) (*models.File, error) {
|
|
var file FileModel
|
|
|
|
if err := i.DB.First(&file, "id = ?", fileID).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !file.IsPublic && file.OwnerID != userID {
|
|
return nil, common.ErrAccessDenied
|
|
}
|
|
|
|
var fileData FileDataModel
|
|
if err := i.DB.First(
|
|
&fileData,
|
|
"file_id = ?",
|
|
fileID,
|
|
).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &models.File{
|
|
ID: file.ID,
|
|
DirID: file.DirID,
|
|
Name: file.Name,
|
|
OwnerID: file.OwnerID,
|
|
IsPublic: file.IsPublic,
|
|
Data: fileData.Data,
|
|
}, nil
|
|
}
|
|
|
|
func splitPath(path string) []string {
|
|
path = strings.Trim(path, "/")
|
|
if path == "" {
|
|
return nil
|
|
}
|
|
return strings.Split(path, "/")
|
|
}
|
|
|
|
func (i *PFSDB) ResolveDirByPath(
|
|
path string,
|
|
userID uuid.UUID,
|
|
) (uuid.UUID, error) {
|
|
|
|
parts := splitPath(path)
|
|
if len(parts) == 0 {
|
|
return uuid.Nil, errors.New("empty path")
|
|
}
|
|
|
|
var current DirModel
|
|
|
|
// --- ищем root ---
|
|
if err := i.DB.Where(
|
|
"name = ? AND parent_id IS NULL AND owner_id = ?",
|
|
parts[0],
|
|
userID,
|
|
).First(¤t).Error; err != nil {
|
|
return uuid.Nil, err
|
|
}
|
|
|
|
// --- идём по дереву ---
|
|
for _, name := range parts[1:] {
|
|
var next DirModel
|
|
|
|
err := i.DB.Where(
|
|
"name = ? AND parent_id = ? AND owner_id = ?",
|
|
name,
|
|
current.ID,
|
|
userID,
|
|
).First(&next).Error
|
|
|
|
if err != nil {
|
|
return uuid.Nil, err
|
|
}
|
|
|
|
current = next
|
|
}
|
|
|
|
return current.ID, nil
|
|
}
|
|
|
|
func (i *PFSDB) ResolveFileByPath(
|
|
path string,
|
|
userID uuid.UUID,
|
|
) (uuid.UUID, error) {
|
|
|
|
parts := splitPath(path)
|
|
if len(parts) < 1 {
|
|
return uuid.Nil, errors.New("invalid path")
|
|
}
|
|
|
|
fileName := parts[len(parts)-1]
|
|
dirPath := parts[:len(parts)-1]
|
|
|
|
var dirID *uuid.UUID
|
|
|
|
// если файл в root
|
|
if len(dirPath) > 0 {
|
|
id, err := i.ResolveDirByPath(
|
|
"/"+strings.Join(dirPath, "/"),
|
|
userID,
|
|
)
|
|
if err != nil {
|
|
return uuid.Nil, err
|
|
}
|
|
dirID = &id
|
|
}
|
|
|
|
var file FileModel
|
|
|
|
err := i.DB.Where(
|
|
"name = ? AND dir_id = ? AND owner_id = ?",
|
|
fileName,
|
|
dirID,
|
|
userID,
|
|
).First(&file).Error
|
|
|
|
if err != nil {
|
|
return uuid.Nil, err
|
|
}
|
|
|
|
return file.ID, nil
|
|
}
|
|
|
|
//
|
|
// ==================== MIGRATION ====================
|
|
//
|
|
|
|
func (i *PFSDB) Migrate() error {
|
|
return i.DB.AutoMigrate(
|
|
&DirModel{},
|
|
&FileModel{},
|
|
&FileDataModel{},
|
|
)
|
|
}
|