feat: add mkdir

This commit is contained in:
d-robotics 2025-05-15 18:09:59 +08:00
parent a2de278199
commit deb3abe394
19 changed files with 541 additions and 184 deletions

84
api_directory.go Normal file
View File

@ -0,0 +1,84 @@
package main
import (
"gosvc/httpserver"
"gosvc/validator"
"robotfs/utils"
)
type Entry struct {
Name string `json:"name"`
IsDir bool `json:"is_dir"`
}
type ListResult struct {
Entries []Entry `json:"entries"`
LastFileName string `json:"last_file_name"`
}
type MkdirParams struct {
Path string
}
func (m *MkdirParams) Validate() error {
return validator.WithIf(
m.Path == "", "Path is empty",
).Validate()
}
func (s *Service) HandleMkdir(
req *httpserver.Request,
resp *httpserver.Response,
) *httpserver.Response {
params := req.Binded.(*MkdirParams)
fullPath := utils.FullPath(params.Path)
err := s.FileSystemManager.MakeDirectory(req.Context(), fullPath)
if err != nil {
return resp.InternalServerError("mkdir failed, " + err.Error())
}
return resp.NoContent()
}
func (s *Service) HandleListDirectory(
req *httpserver.Request,
resp *httpserver.Response,
) *httpserver.Response {
// path := req.QueryString("path")
// startFileName := req.QueryString("startFileName")
// limit := req.QueryInt("limit")
entries := []Entry{
{Name: "file1.txt", IsDir: false},
{Name: "folder1", IsDir: true},
}
lastFileName := "file1.txt"
result := ListResult{
Entries: entries,
LastFileName: lastFileName,
}
return resp.OK(result).JSON()
}
func (s *Service) HandleDeleteDirectory(
req *httpserver.Request,
resp *httpserver.Response,
) *httpserver.Response {
return resp.NoContent()
}
func (s *Service) HandleRenameDirectory(
req *httpserver.Request,
resp *httpserver.Response,
) *httpserver.Response {
return resp.NoContent()
}
func (s *Service) HandleCopyDirectory(
req *httpserver.Request,
resp *httpserver.Response,
) *httpserver.Response {
return resp.NoContent()
}

84
api_file.go Normal file
View File

@ -0,0 +1,84 @@
package main
import (
"gosvc/httpserver"
"gosvc/validator"
)
type DeleteParams struct {
Path string
IsDir bool
}
func (d *DeleteParams) Validate() error {
return validator.WithIf(
d.Path == "", "Path is empty",
).Validate()
}
type GeneralParams struct {
SrcPath string
DstPath string
IsDir bool
}
func (g *GeneralParams) Validate() error {
return validator.ChainValidate(
validator.WithIf(
g.SrcPath == "", "SrcPath is empty",
),
validator.WithIf(
g.DstPath == "", "DstPath is empty",
),
)
}
type CopyParams struct {
GeneralParams
}
type RenameParams struct {
GeneralParams
}
func (s *Service) HandleUploadFile(
req *httpserver.Request,
resp *httpserver.Response,
) *httpserver.Response {
return resp.NoContent()
}
func (s *Service) HandleInfoFile(
req *httpserver.Request,
resp *httpserver.Response,
) *httpserver.Response {
return resp.NoContent()
}
func (s *Service) HandleDownloadFile(
req *httpserver.Request,
resp *httpserver.Response,
) *httpserver.Response {
return resp.NoContent()
}
func (s *Service) HandleDeleteFile(
req *httpserver.Request,
resp *httpserver.Response,
) *httpserver.Response {
return resp.NoContent()
}
func (s *Service) HandleRenameFile(
req *httpserver.Request,
resp *httpserver.Response,
) *httpserver.Response {
return resp.NoContent()
}
func (s *Service) HandleCopyFile(
req *httpserver.Request,
resp *httpserver.Response,
) *httpserver.Response {
return resp.NoContent()
}

View File

@ -4,7 +4,7 @@ import (
"gosvc/httpserver" "gosvc/httpserver"
) )
func (s *Service) API_Ping( func (s *Service) HandlePing(
req *httpserver.Request, req *httpserver.Request,
resp *httpserver.Response, resp *httpserver.Response,
) *httpserver.Response { ) *httpserver.Response {

0
config.yml Normal file
View File

33
config/robotfs.go Normal file
View File

@ -0,0 +1,33 @@
package config
type RobotFSConfig struct {
Address string `mapstructure:"address"`
LogPath string `mapstructure:"log_path"`
}
type RedisConfig struct {
Address string `mapstructure:"address"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
type S3Config struct {
Region string `mapstructure:"region"`
Endpoint string `mapstructure:"endpoint"`
AccessKeyID string `mapstructure:"access_key_id"`
SecretAccessKey string `mapstructure:"secret_access_key"`
BucketName string `mapstructure:"bucket_name"`
UsePathStyle bool `mapstructure:"use_path_style"`
}
type Config struct {
RobotFS RobotFSConfig `mapstructure:"robotfs"`
Redis RedisConfig `mapstructure:"redis"`
S3 S3Config `mapstructure:"s3"`
}
func GetConfig() *Config {
config := &Config{}
return config
}

View File

@ -1,29 +0,0 @@
package config
type Config struct {
Robotfs struct {
Address string `mapstructure:"ADDRESS"`
LogPath string `mapstructure:"LOGPATH"`
} `mapstructure:"robotfs"`
Redis struct {
Address string `mapstructure:"address"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
} `mapstructure:"redis"`
S3 struct {
Region string `mapstructure:"region"`
Endpoint string `mapstructure:"endpoint"`
AccessKeyID string `mapstructure:"access_key_id"`
SecretAccessKey string `mapstructure:"secret_access_key"`
BucketName string `mapstructure:"bucket_name"`
UsePathStyle bool `mapstructure:"use_path_style"`
} `mapstructure:"s3"`
}
func GetConfig() *Config {
config := &Config{}
return config
}

View File

@ -8,8 +8,7 @@ import (
) )
type Engine struct { type Engine struct {
metadataManager *MetaDataManager FileSystemManager *FileSystemManager
storageManager *StorageManager
} }
func NewEngine() *Engine { func NewEngine() *Engine {
@ -17,31 +16,25 @@ func NewEngine() *Engine {
} }
func (e *Engine) Start() error { func (e *Engine) Start() error {
store := redis_lua.NewRedisLuaStore()
if err := store.Initialize(utils.GetViper(), "redis."); err != nil {
return fmt.Errorf("failed to initialize meta store: %v", err)
}
metadata, err := NewMetaDataManager(store)
if err != nil {
return fmt.Errorf("failed to create metadata manager: %v", err)
}
e.metadataManager = metadata
s3Config, err := Initialize(utils.GetViper(), "s3.") s3Config, err := Initialize(utils.GetViper(), "s3.")
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize s3 config: %v", err) return fmt.Errorf("failed to initialize s3 config: %v", err)
} }
storageManager, err := NewStorageManager(s3Config) object, err := NewStorageManager(s3Config)
if err != nil { if err != nil {
return fmt.Errorf("failed to create storage manager: %v", err) return fmt.Errorf("failed to create storage manager: %v", err)
} }
e.storageManager = storageManager
meta := redis_lua.NewRedisLuaStore()
if err := meta.Initialize(utils.GetViper(), "redis."); err != nil {
return fmt.Errorf("failed to initialize meta store: %v", err)
}
filesystem := NewFileSystemManager(meta, object)
e.FileSystemManager = filesystem
return nil return nil
} }
func (e *Engine) Stop() { func (e *Engine) Stop() {
if e.metadataManager != nil { e.FileSystemManager.Shutdown()
e.metadataManager.Shutdown()
}
} }

View File

@ -0,0 +1,85 @@
package engine
import (
"context"
"fmt"
"time"
"robotfs/pb"
"robotfs/store"
"robotfs/utils"
)
type FileSystemManager struct {
meta store.MetaStore
storage *StorageManager
}
func NewFileSystemManager(meta store.MetaStore, storage *StorageManager) *FileSystemManager {
return &FileSystemManager{
meta: meta,
storage: storage,
}
}
func (f *FileSystemManager) BeginTransaction(ctx context.Context) (context.Context, error) {
return f.meta.BeginTransaction(ctx)
}
func (f *FileSystemManager) CommitTransaction(ctx context.Context) error {
return f.meta.CommitTransaction(ctx)
}
func (f *FileSystemManager) RollbackTransaction(ctx context.Context) error {
return f.meta.RollbackTransaction(ctx)
}
var (
Root = &utils.Entry{
FullPath: utils.FullPath("/"),
IsDir: true,
CreateTime: time.Now().Unix(),
LastModificationTime: time.Now().Unix(),
}
)
func (f *FileSystemManager) FindEntry(ctx context.Context, p utils.FullPath) (entry *pb.FileEntry, err error) {
if p == "/" {
return nil, nil
}
entry, err = f.meta.FindEntry(ctx, p)
if err != nil {
return nil, err
}
return
}
func (f *FileSystemManager) MakeDirectory(ctx context.Context, path utils.FullPath) error {
if string(path) == "/" {
return nil
}
if entry, _ := f.FindEntry(ctx, path); entry != nil {
return fmt.Errorf("directory %s already exists", path)
}
parentDir, _ := path.DirAndName()
if parentDir != "/" {
if parentEntry, _ := f.FindEntry(ctx, utils.FullPath(parentDir)); parentEntry == nil {
return fmt.Errorf("parent directory %s does not exist", parentDir)
}
}
entry := utils.MakeEntry(path, true)
return f.meta.InsertEntry(ctx, entry)
}
func (f *FileSystemManager) Shutdown() error {
if f.meta != nil {
f.meta.Shutdown()
}
return nil
}

View File

@ -1,28 +0,0 @@
package engine
import (
"fmt"
"robotfs/store"
)
type MetaDataManager struct {
store store.MetaStore
}
func NewMetaDataManager(store store.MetaStore) (*MetaDataManager, error) {
if store == nil {
return nil, fmt.Errorf("meta store is required")
}
return &MetaDataManager{
store: store,
}, nil
}
func (m *MetaDataManager) Shutdown() error {
if m.store != nil {
m.store.Shutdown()
}
return nil
}

View File

@ -1,42 +0,0 @@
package engine
import (
"context"
"time"
)
type WorkSpaceManager struct{}
func NewWorkSpaceManager() *WorkSpaceManager {
return &WorkSpaceManager{}
}
type StorageSpace struct {
ID string
Name string
OwnerID string
Type string
CreateTime time.Time
UpdateTime time.Time
InviteCode string
RootPath string
}
func (wsm *WorkSpaceManager) CreateWorkSpace(ctx context.Context, ownerID, name, visibility string) (string, error) {
return "", nil
}
func (wsm *WorkSpaceManager) GetWorkSpace(ctx context.Context, spaceID string) {
}
func (wsm *WorkSpaceManager) UpdateWorkSpace(ctx context.Context, spaceID string) (*StorageSpace, error) {
return nil, nil
}
func (wsm *WorkSpaceManager) GenerateInviteCode(ctx context.Context, spaceID string) (string, error) {
return "", nil
}
func (wsm *WorkSpaceManager) ImportWorkSpace(ctx context.Context, code string, newOwnerID string) (string, error) {
return "", nil
}

14
main.go
View File

@ -20,14 +20,17 @@ Options:
Example: Example:
robotfs --config=/path/to/config.yaml` robotfs --config=/path/to/config.yaml`
var (
configFile = flag.String("config", "", "path to config file")
version = flag.Bool("version", false, "show version information")
help = flag.Bool("help", false, "show help message")
)
func printUsage() { func printUsage() {
fmt.Println(usage) fmt.Println(usage)
} }
func main() { func main() {
configFile := flag.String("config", "", "path to config file")
version := flag.Bool("version", false, "show version information")
help := flag.Bool("help", false, "show help message")
flag.Parse() flag.Parse()
if *help { if *help {
@ -41,8 +44,9 @@ func main() {
} }
if *configFile != "" { if *configFile != "" {
utils.ConfigurationFileDirectory.Set(*configFile) err := utils.LoadConfiguration(*configFile)
if !utils.LoadConfiguration("config", true) { if err != nil {
fmt.Println("load config file failed, " + err.Error())
os.Exit(1) os.Exit(1)
} }
} else { } else {

110
router.go
View File

@ -12,7 +12,115 @@ func (s *Service) RegisterRouteRules() {
{ {
Path: "/ping", Path: "/ping",
Method: http.MethodGet, Method: http.MethodGet,
Handler: s.API_Ping, Handler: s.HandlePing,
},
{
Path: "/robotfs",
SubRules: []*httpserver.RouteRule{
{
Path: "/mkdir",
Method: http.MethodPost,
Handler: s.HandleMkdir,
Bind: &MkdirParams{},
},
{
Path: "/dir/list",
Method: http.MethodGet,
Handler: s.HandleListDirectory,
QueryRules: []*httpserver.QueryRule{
{
Key: "path",
Type: httpserver.QueryTypeString,
Required: true,
},
{
Key: "startFileName",
Type: httpserver.QueryTypeString,
Required: true,
},
{
Key: "limit",
Type: httpserver.QueryTypeInt,
Required: true,
Min: 1,
Max: 2048,
},
},
},
{
Path: "/dir/delete",
Method: http.MethodPost,
Handler: s.HandleDeleteDirectory,
Bind: &DeleteParams{},
},
{
Path: "/dir/rename",
Method: http.MethodPost,
Handler: s.HandleRenameDirectory,
Bind: &RenameParams{},
},
// Todo: need juicefs clone
{
Path: "/dir/copy",
Method: http.MethodPost,
Handler: s.HandleCopyDirectory,
Bind: &CopyParams{},
},
{
Path: "/file/upload",
Method: http.MethodPost,
Handler: s.HandleUploadFile,
QueryRules: []*httpserver.QueryRule{
{
Key: "path",
Type: httpserver.QueryTypeString,
Required: true,
},
},
},
{
Path: "/info",
Method: http.MethodGet,
Handler: s.HandleInfoFile,
QueryRules: []*httpserver.QueryRule{
{
Key: "path",
Type: httpserver.QueryTypeString,
Required: true,
},
},
},
{
Path: "/file/download",
Method: http.MethodGet,
Handler: s.HandleDownloadFile,
QueryRules: []*httpserver.QueryRule{
{
Key: "path",
Type: httpserver.QueryTypeString,
Required: true,
},
},
},
{
Path: "/file/delete",
Method: http.MethodPost,
Handler: s.HandleDeleteFile,
Bind: &DeleteParams{},
},
{
Path: "/file/rename",
Method: http.MethodPost,
Handler: s.HandleRenameFile,
Bind: &RenameParams{},
},
{
Path: "/file/copy",
Method: http.MethodPost,
Handler: s.HandleCopyFile,
Bind: &CopyParams{},
},
},
}, },
}, },
) )

View File

@ -10,17 +10,17 @@ import (
type Service struct { type Service struct {
gosvc.ServiceBase gosvc.ServiceBase
engine *engine.Engine *engine.Engine
} }
func CreateService() (*Service, error) { func CreateService() (*Service, error) {
return &Service{ return &Service{
engine: engine.NewEngine(), Engine: engine.NewEngine(),
}, nil }, nil
} }
func (s *Service) Start() error { func (s *Service) Start() error {
if err := s.engine.Start(); err != nil { if err := s.Engine.Start(); err != nil {
return fmt.Errorf("failed to start engine: %v", err) return fmt.Errorf("failed to start engine: %v", err)
} }
@ -28,8 +28,5 @@ func (s *Service) Start() error {
} }
func (s *Service) Stop() { func (s *Service) Stop() {
s.engine.Stop() s.Engine.Stop()
}
func (s *Service) Tick() {
} }

View File

@ -2,20 +2,27 @@ package store
import ( import (
"context" "context"
"errors"
"robotfs/pb" "robotfs/pb"
"robotfs/utils" "robotfs/utils"
) )
type ListEachEntryFunc func(entry *pb.FileEntry) bool var (
ErrUnsupportedListDirectoryPrefixed = errors.New("unsupported directory prefix listing")
ErrKvNotImplemented = errors.New("kv not implemented yet")
ErrKvNotFound = errors.New("kv: not found")
)
type ListEachEntryFunc func(entry *utils.Entry) bool
type MetaStore interface { type MetaStore interface {
GetName() string GetName() string
Initialize(configuration utils.Configuration, prefix string) error Initialize(configuration utils.Configuration, prefix string) error
InsertEntry(context.Context, *pb.FileEntry) error InsertEntry(context.Context, *utils.Entry) error
UpdateEntry(context.Context, *pb.FileEntry) (err error) UpdateEntry(context.Context, *utils.Entry) (err error)
FindEntry(context.Context, utils.FullPath) (entry *pb.FileEntry, err error) FindEntry(context.Context, utils.FullPath) (entry *pb.FileEntry, err error)
DeleteEntry(context.Context, utils.FullPath) (err error) DeleteEntry(context.Context, utils.FullPath) (err error)
DeleteFolderChildren(context.Context, utils.FullPath) (err error) DeleteFolderChildren(context.Context, utils.FullPath) (err error)

View File

@ -23,14 +23,16 @@ func (store *RedisLuaStore) Initialize(configuration utils.Configuration, prefix
configuration.GetString(prefix+"address"), configuration.GetString(prefix+"address"),
configuration.GetString(prefix+"password"), configuration.GetString(prefix+"password"),
configuration.GetInt(prefix+"database"), configuration.GetInt(prefix+"database"),
configuration.GetStringSlice(prefix+"superLargeDirectories"),
) )
} }
func (store *RedisLuaStore) initialize(hostPort string, password string, database int) (err error) { func (store *RedisLuaStore) initialize(hostPort string, password string, database int, superLargeDirectories []string) (err error) {
store.Client = redis.NewClient(&redis.Options{ store.Client = redis.NewClient(&redis.Options{
Addr: hostPort, Addr: hostPort,
Password: password, Password: password,
DB: database, DB: database,
}) })
store.loadSuperLargeDirectories(superLargeDirectories)
return return
} }

View File

@ -43,32 +43,27 @@ func (store *UniversalRedisLuaStore) RollbackTransaction(ctx context.Context) er
return nil return nil
} }
func (store *UniversalRedisLuaStore) InsertEntry(ctx context.Context, entry *pb.FileEntry) (err error) { func (store *UniversalRedisLuaStore) InsertEntry(ctx context.Context, entry *utils.Entry) (err error) {
value, err := entry.Encode()
if err != nil {
return fmt.Errorf("encoding %s: %v", entry.FullPath, err)
}
// value, err := entry.EncodeAttributesAndChunks() dir, name := entry.FullPath.DirAndName()
// if err != nil {
// return fmt.Errorf("encoding %s %+v: %v", entry.FullPath, entry.Attr, err)
// }
// if len(entry.GetChunks()) > CountEntryChunksForGzip { err = stored_procedure.InsertEntryScript.Run(ctx, store.Client,
// value = util.MaybeGzipData(value) []string{string(entry.FullPath), genDirectoryListKey(dir)},
// } value, 0,
store.isSuperLargeDirectory(dir), 0, name).Err()
// dir, name := entry.FullPath.DirAndName() if err != nil {
return fmt.Errorf("persisting %s : %v", entry.FullPath, err)
// err = stored_procedure.InsertEntryScript.Run(ctx, store.Client, }
// []string{string(entry.FullPath), genDirectoryListKey(dir)},
// value, entry.TtlSec,
// store.isSuperLargeDirectory(dir), 0, name).Err()
// if err != nil {
// return fmt.Errorf("persisting %s : %v", entry.FullPath, err)
// }
return nil return nil
} }
func (store *UniversalRedisLuaStore) UpdateEntry(ctx context.Context, entry *pb.FileEntry) (err error) { func (store *UniversalRedisLuaStore) UpdateEntry(ctx context.Context, entry *utils.Entry) (err error) {
return store.InsertEntry(ctx, entry) return store.InsertEntry(ctx, entry)
} }

View File

@ -1,19 +1,13 @@
package utils package utils
import ( import (
"path/filepath"
"strings" "strings"
"sync" "sync"
"gosvc/logger"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
var (
ConfigurationFileDirectory DirectoryValueType
loadSecurityConfigOnce sync.Once
)
type DirectoryValueType string type DirectoryValueType string
func (s *DirectoryValueType) Set(value string) error { func (s *DirectoryValueType) Set(value string) error {
@ -32,33 +26,29 @@ type Configuration interface {
SetDefault(key string, value interface{}) SetDefault(key string, value interface{})
} }
func LoadSecurityConfiguration() { func LoadConfiguration(configFilePath string) error {
loadSecurityConfigOnce.Do(func() { dir := filepath.Dir(configFilePath)
LoadConfiguration("security", false) base := filepath.Base(configFilePath)
}) ext := filepath.Ext(base)
configName := strings.TrimSuffix(base, ext)
viper.SetConfigName(configName)
viper.AddConfigPath(dir)
switch ext {
case ".yaml", ".yml":
viper.SetConfigType("yaml")
case ".json":
viper.SetConfigType("json")
case ".toml":
viper.SetConfigType("toml")
} }
func LoadConfiguration(configFileName string, required bool) (loaded bool) { if err := viper.MergeInConfig(); err != nil {
viper.SetConfigName(configFileName) // name of config file (without extension) return err
viper.AddConfigPath(".") // look for config in the working directory }
if err := viper.MergeInConfig(); err != nil { // Handle errors reading the config file return nil
if strings.Contains(err.Error(), "Not Found") {
logger.Info("Reading %s: %v", viper.ConfigFileUsed(), err)
} else {
logger.Error("Reading %s: %v", viper.ConfigFileUsed(), err)
}
if required {
logger.Error("Failed to load %s.yaml file from current directory\n"+
"\nPlease use this command to run the program:\n"+
" robotfs --config=%s.yaml\n\n\n",
configFileName, configFileName)
}
return false
}
logger.Info("Reading %s.yaml from %s", configFileName, viper.ConfigFileUsed())
return true
} }
type ViperProxy struct { type ViperProxy struct {

74
utils/entry.go Normal file
View File

@ -0,0 +1,74 @@
package utils
import (
"fmt"
"robotfs/pb"
"time"
"google.golang.org/protobuf/proto"
)
type Entry struct {
FullPath FullPath
IsDir bool
Size uint64
CreateTime int64
S3Key string
ContentType string
Etag string
VersionID string
LastModificationTime int64
Extended map[string][]byte
}
func MakeEntry(fullPath FullPath, isDirectory bool) *Entry {
return &Entry{
FullPath: fullPath,
IsDir: isDirectory,
CreateTime: time.Now().Unix(),
LastModificationTime: time.Now().Unix(),
}
}
func (entry *Entry) Encode() ([]byte, error) {
message := entry.ToProto()
return proto.Marshal(message)
}
func (entry *Entry) Decode(blob []byte) error {
message := &pb.FileEntry{}
if err := proto.Unmarshal(blob, message); err != nil {
return fmt.Errorf("decoding value blob for %s: %v", entry.FullPath, err)
}
entry.FromProto(message)
return nil
}
func (entry *Entry) ToProto() *pb.FileEntry {
return &pb.FileEntry{
FullPath: string(entry.FullPath),
IsDir: entry.IsDir,
Size: entry.Size,
CreateTime: entry.CreateTime,
S3Key: entry.S3Key,
ContentType: entry.ContentType,
Etag: entry.Etag,
VersionId: entry.VersionID,
LastModificationTime: entry.LastModificationTime,
Extended: entry.Extended,
}
}
func (entry *Entry) FromProto(pb *pb.FileEntry) {
entry.FullPath = FullPath(pb.FullPath)
entry.IsDir = pb.IsDir
entry.Size = pb.Size
entry.CreateTime = pb.CreateTime
entry.S3Key = pb.S3Key
entry.ContentType = pb.ContentType
entry.Etag = pb.Etag
entry.VersionID = pb.VersionId
entry.LastModificationTime = pb.LastModificationTime
entry.Extended = pb.Extended
}

View File

@ -1,8 +1,8 @@
package utils package utils
import ( import (
"strings"
"path/filepath" "path/filepath"
"strings"
) )
func NormalizePath(path string) string { func NormalizePath(path string) string {