diff options
| author | diogo464 <[email protected]> | 2025-08-07 13:49:25 +0100 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-08-07 13:49:25 +0100 |
| commit | 9a25abd1d6ef6f5b0e2c08751183f63db43c73a5 (patch) | |
| tree | da4c902ee86df1c0c4b875bc1697dd8b08590123 | |
init
| -rwxr-xr-x | auth.sh | 8 | ||||
| -rw-r--r-- | deployment.yaml | 28 | ||||
| -rw-r--r-- | filesystem.go | 244 | ||||
| -rw-r--r-- | go.mod | 8 | ||||
| -rw-r--r-- | go.sum | 4 | ||||
| -rw-r--r-- | index.html | 17 | ||||
| -rw-r--r-- | main.go | 75 | ||||
| -rw-r--r-- | operation.go | 147 | ||||
| -rw-r--r-- | path.go | 75 | ||||
| -rw-r--r-- | service.yaml | 0 |
10 files changed, 606 insertions, 0 deletions
| @@ -0,0 +1,8 @@ | |||
| 1 | #!/usr/bin/env bash | ||
| 2 | |||
| 3 | podman run -it --rm --network host \ | ||
| 4 | -e SECRET="12345678912345678911111111111111" \ | ||
| 5 | -e APP_URL="http://10.0.0.92:3000" \ | ||
| 6 | -e USERS='diogo464:$2a$10$fE78J/Rq7kSik1cmXQ82Be6.zx3P4GEjhlifnI.ACHpHb5sDH/J1W' \ | ||
| 7 | ghcr.io/steveiliop56/tinyauth:v3 | ||
| 8 | |||
diff --git a/deployment.yaml b/deployment.yaml new file mode 100644 index 0000000..3bcd9cc --- /dev/null +++ b/deployment.yaml | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | --- | ||
| 2 | apiVersion: apps/v1 | ||
| 3 | kind: Deployment | ||
| 4 | metadata: | ||
| 5 | name: auth | ||
| 6 | spec: | ||
| 7 | selector: | ||
| 8 | matchLabels: | ||
| 9 | app: auth | ||
| 10 | template: | ||
| 11 | metadata: | ||
| 12 | labels: | ||
| 13 | app: auth | ||
| 14 | app.kubernetes.io/name: auth | ||
| 15 | spec: | ||
| 16 | containers: | ||
| 17 | - name: auth | ||
| 18 | image: ghcr.io/steveiliop56/tinyauth:v3 | ||
| 19 | resources: | ||
| 20 | requests: | ||
| 21 | memory: "64Mi" | ||
| 22 | cpu: "0" | ||
| 23 | limits: | ||
| 24 | memory: "256Mi" | ||
| 25 | cpu: "1" | ||
| 26 | ports: | ||
| 27 | - name: http | ||
| 28 | containerPort: 8000 | ||
diff --git a/filesystem.go b/filesystem.go new file mode 100644 index 0000000..236f704 --- /dev/null +++ b/filesystem.go | |||
| @@ -0,0 +1,244 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import "fmt" | ||
| 4 | |||
| 5 | type Filesystem_Ent_Type string | ||
| 6 | |||
| 7 | const ( | ||
| 8 | Filesystem_Ent_Type_Dir Filesystem_Ent_Type = "dir" | ||
| 9 | Filesystem_Ent_Type_File Filesystem_Ent_Type = "file" | ||
| 10 | ) | ||
| 11 | |||
| 12 | type Filesystem struct { | ||
| 13 | Root *Filesystem_Ent | ||
| 14 | } | ||
| 15 | |||
| 16 | func NewFilesystem() *Filesystem { | ||
| 17 | return &Filesystem{ | ||
| 18 | Root: &Filesystem_Ent{ | ||
| 19 | Type: Filesystem_Ent_Type_Dir, | ||
| 20 | Children: make(map[PathComponent]*Filesystem_Ent), | ||
| 21 | }, | ||
| 22 | } | ||
| 23 | } | ||
| 24 | |||
| 25 | type Filesystem_Ent struct { | ||
| 26 | Type Filesystem_Ent_Type | ||
| 27 | Children map[PathComponent]*Filesystem_Ent | ||
| 28 | Blob string | ||
| 29 | } | ||
| 30 | |||
| 31 | func (fs *Filesystem) GetEntry(path Path) *Filesystem_Ent { | ||
| 32 | current := fs.Root | ||
| 33 | for _, component := range path.Components() { | ||
| 34 | if current == nil || current.Type != Filesystem_Ent_Type_Dir { | ||
| 35 | return nil | ||
| 36 | } | ||
| 37 | current = current.Children[component] | ||
| 38 | } | ||
| 39 | return current | ||
| 40 | } | ||
| 41 | |||
| 42 | func (fs *Filesystem) EnsureParentDirs(path Path) error { | ||
| 43 | if path.IsRoot() { | ||
| 44 | return nil | ||
| 45 | } | ||
| 46 | |||
| 47 | parentPath := path.Parent() | ||
| 48 | current := fs.Root | ||
| 49 | |||
| 50 | for _, component := range parentPath.Components() { | ||
| 51 | if current.Children == nil { | ||
| 52 | current.Children = make(map[PathComponent]*Filesystem_Ent) | ||
| 53 | } | ||
| 54 | |||
| 55 | if next, exists := current.Children[component]; exists { | ||
| 56 | if next.Type != Filesystem_Ent_Type_Dir { | ||
| 57 | return fmt.Errorf("path component '%s' is a file, cannot create directory", component) | ||
| 58 | } | ||
| 59 | current = next | ||
| 60 | } else { | ||
| 61 | newDir := &Filesystem_Ent{ | ||
| 62 | Type: Filesystem_Ent_Type_Dir, | ||
| 63 | Children: make(map[PathComponent]*Filesystem_Ent), | ||
| 64 | } | ||
| 65 | current.Children[component] = newDir | ||
| 66 | current = newDir | ||
| 67 | } | ||
| 68 | } | ||
| 69 | |||
| 70 | return nil | ||
| 71 | } | ||
| 72 | |||
| 73 | func (fs *Filesystem) ApplyOperation(op *Operation) error { | ||
| 74 | switch op.Type { | ||
| 75 | case Operation_Type_CreateFile: | ||
| 76 | return fs.ApplyOperation_CreateFile(op) | ||
| 77 | case Operation_Type_CreateDir: | ||
| 78 | return fs.ApplyOperation_CreateDir(op) | ||
| 79 | case Operation_Type_Remove: | ||
| 80 | return fs.ApplyOperation_Remove(op) | ||
| 81 | case Operation_Type_Rename: | ||
| 82 | return fs.ApplyOperation_Rename(op) | ||
| 83 | default: | ||
| 84 | return fmt.Errorf("unhandled operation type: %s", op.Type) | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | func (fs *Filesystem) ApplyOperation_CreateFile(op *Operation) error { | ||
| 89 | params := op.GetCreateFileParams() | ||
| 90 | |||
| 91 | path, err := ParsePath(params.Path) | ||
| 92 | if err != nil { | ||
| 93 | return fmt.Errorf("invalid path '%s': %w", params.Path, err) | ||
| 94 | } | ||
| 95 | |||
| 96 | if path.IsRoot() { | ||
| 97 | return fmt.Errorf("cannot create file at root path") | ||
| 98 | } | ||
| 99 | |||
| 100 | if err := fs.EnsureParentDirs(path); err != nil { | ||
| 101 | return fmt.Errorf("failed to create parent directories: %w", err) | ||
| 102 | } | ||
| 103 | |||
| 104 | parentPath := path.Parent() | ||
| 105 | parent := fs.GetEntry(parentPath) | ||
| 106 | if parent == nil || parent.Type != Filesystem_Ent_Type_Dir { | ||
| 107 | return fmt.Errorf("parent is not a directory") | ||
| 108 | } | ||
| 109 | |||
| 110 | if parent.Children == nil { | ||
| 111 | parent.Children = make(map[PathComponent]*Filesystem_Ent) | ||
| 112 | } | ||
| 113 | |||
| 114 | filename := path.Components()[len(path.Components())-1] | ||
| 115 | parent.Children[filename] = &Filesystem_Ent{ | ||
| 116 | Type: Filesystem_Ent_Type_File, | ||
| 117 | Blob: params.Blob, | ||
| 118 | } | ||
| 119 | |||
| 120 | return nil | ||
| 121 | } | ||
| 122 | |||
| 123 | func (fs *Filesystem) ApplyOperation_CreateDir(op *Operation) error { | ||
| 124 | params := op.GetCreateDirParams() | ||
| 125 | |||
| 126 | path, err := ParsePath(params.Path) | ||
| 127 | if err != nil { | ||
| 128 | return fmt.Errorf("invalid path '%s': %w", params.Path, err) | ||
| 129 | } | ||
| 130 | |||
| 131 | if path.IsRoot() { | ||
| 132 | return nil | ||
| 133 | } | ||
| 134 | |||
| 135 | existing := fs.GetEntry(path) | ||
| 136 | if existing != nil { | ||
| 137 | if existing.Type == Filesystem_Ent_Type_Dir { | ||
| 138 | return nil | ||
| 139 | } | ||
| 140 | return fmt.Errorf("cannot create directory '%s': file already exists", params.Path) | ||
| 141 | } | ||
| 142 | |||
| 143 | if err := fs.EnsureParentDirs(path); err != nil { | ||
| 144 | return fmt.Errorf("failed to create parent directories: %w", err) | ||
| 145 | } | ||
| 146 | |||
| 147 | parentPath := path.Parent() | ||
| 148 | parent := fs.GetEntry(parentPath) | ||
| 149 | if parent == nil || parent.Type != Filesystem_Ent_Type_Dir { | ||
| 150 | return fmt.Errorf("parent is not a directory") | ||
| 151 | } | ||
| 152 | |||
| 153 | if parent.Children == nil { | ||
| 154 | parent.Children = make(map[PathComponent]*Filesystem_Ent) | ||
| 155 | } | ||
| 156 | |||
| 157 | dirname := path.Components()[len(path.Components())-1] | ||
| 158 | parent.Children[dirname] = &Filesystem_Ent{ | ||
| 159 | Type: Filesystem_Ent_Type_Dir, | ||
| 160 | Children: make(map[PathComponent]*Filesystem_Ent), | ||
| 161 | } | ||
| 162 | |||
| 163 | return nil | ||
| 164 | } | ||
| 165 | |||
| 166 | func (fs *Filesystem) ApplyOperation_Remove(op *Operation) error { | ||
| 167 | params := op.GetRemoveParams() | ||
| 168 | |||
| 169 | path, err := ParsePath(params.Path) | ||
| 170 | if err != nil { | ||
| 171 | return fmt.Errorf("invalid path '%s': %w", params.Path, err) | ||
| 172 | } | ||
| 173 | |||
| 174 | if path.IsRoot() { | ||
| 175 | return fmt.Errorf("cannot remove root directory") | ||
| 176 | } | ||
| 177 | |||
| 178 | existing := fs.GetEntry(path) | ||
| 179 | if existing == nil { | ||
| 180 | return nil | ||
| 181 | } | ||
| 182 | |||
| 183 | parentPath := path.Parent() | ||
| 184 | parent := fs.GetEntry(parentPath) | ||
| 185 | if parent == nil || parent.Type != Filesystem_Ent_Type_Dir { | ||
| 186 | return fmt.Errorf("parent is not a directory") | ||
| 187 | } | ||
| 188 | |||
| 189 | filename := path.Components()[len(path.Components())-1] | ||
| 190 | delete(parent.Children, filename) | ||
| 191 | |||
| 192 | return nil | ||
| 193 | } | ||
| 194 | |||
| 195 | func (fs *Filesystem) ApplyOperation_Rename(op *Operation) error { | ||
| 196 | params := op.GetRenameParams() | ||
| 197 | |||
| 198 | oldPath, err := ParsePath(params.Old) | ||
| 199 | if err != nil { | ||
| 200 | return fmt.Errorf("invalid old path '%s': %w", params.Old, err) | ||
| 201 | } | ||
| 202 | |||
| 203 | newPath, err := ParsePath(params.New) | ||
| 204 | if err != nil { | ||
| 205 | return fmt.Errorf("invalid new path '%s': %w", params.New, err) | ||
| 206 | } | ||
| 207 | |||
| 208 | if oldPath.IsRoot() || newPath.IsRoot() { | ||
| 209 | return fmt.Errorf("cannot rename root directory") | ||
| 210 | } | ||
| 211 | |||
| 212 | existing := fs.GetEntry(oldPath) | ||
| 213 | if existing == nil { | ||
| 214 | return nil | ||
| 215 | } | ||
| 216 | |||
| 217 | if err := fs.EnsureParentDirs(newPath); err != nil { | ||
| 218 | return fmt.Errorf("failed to create parent directories for new path: %w", err) | ||
| 219 | } | ||
| 220 | |||
| 221 | oldParentPath := oldPath.Parent() | ||
| 222 | oldParent := fs.GetEntry(oldParentPath) | ||
| 223 | if oldParent == nil || oldParent.Type != Filesystem_Ent_Type_Dir { | ||
| 224 | return fmt.Errorf("old parent is not a directory") | ||
| 225 | } | ||
| 226 | |||
| 227 | newParentPath := newPath.Parent() | ||
| 228 | newParent := fs.GetEntry(newParentPath) | ||
| 229 | if newParent == nil || newParent.Type != Filesystem_Ent_Type_Dir { | ||
| 230 | return fmt.Errorf("new parent is not a directory") | ||
| 231 | } | ||
| 232 | |||
| 233 | if newParent.Children == nil { | ||
| 234 | newParent.Children = make(map[PathComponent]*Filesystem_Ent) | ||
| 235 | } | ||
| 236 | |||
| 237 | oldFilename := oldPath.Components()[len(oldPath.Components())-1] | ||
| 238 | newFilename := newPath.Components()[len(newPath.Components())-1] | ||
| 239 | |||
| 240 | newParent.Children[newFilename] = existing | ||
| 241 | delete(oldParent.Children, oldFilename) | ||
| 242 | |||
| 243 | return nil | ||
| 244 | } | ||
| @@ -0,0 +1,8 @@ | |||
| 1 | module git.d464.sh/fctdrive | ||
| 2 | |||
| 3 | go 1.24.5 | ||
| 4 | |||
| 5 | require ( | ||
| 6 | github.com/go-chi/chi/v5 v5.2.2 | ||
| 7 | github.com/pkg/errors v0.9.1 | ||
| 8 | ) | ||
| @@ -0,0 +1,4 @@ | |||
| 1 | github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= | ||
| 2 | github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= | ||
| 3 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||
| 4 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||
diff --git a/index.html b/index.html new file mode 100644 index 0000000..169ebc0 --- /dev/null +++ b/index.html | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | <!DOCTYPE html> | ||
| 2 | <html lang="en"> | ||
| 3 | |||
| 4 | <head> | ||
| 5 | <meta charset="UTF-8"> | ||
| 6 | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
| 7 | <title>FCT Drive</title> | ||
| 8 | </head> | ||
| 9 | |||
| 10 | <body> | ||
| 11 | <form enctype="multipart/form-data" method="post" action="/upload"> | ||
| 12 | <input type="file" name="file"> | ||
| 13 | <input type="submit" name="submit" value="Upload"> | ||
| 14 | </form> | ||
| 15 | </body> | ||
| 16 | |||
| 17 | </html> | ||
| @@ -0,0 +1,75 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "encoding/json" | ||
| 5 | "net/http" | ||
| 6 | "os" | ||
| 7 | |||
| 8 | "github.com/go-chi/chi/v5" | ||
| 9 | ) | ||
| 10 | |||
| 11 | type App struct { | ||
| 12 | Operations []*Operation | ||
| 13 | Filesystem *Filesystem | ||
| 14 | } | ||
| 15 | |||
| 16 | func main() { | ||
| 17 | router := chi.NewRouter() | ||
| 18 | |||
| 19 | app := &App{ | ||
| 20 | Operations: []*Operation{}, | ||
| 21 | Filesystem: NewFilesystem(), | ||
| 22 | } | ||
| 23 | |||
| 24 | router.Get("/", func(w http.ResponseWriter, r *http.Request) { | ||
| 25 | w.Header().Add("Content-Type", "text/html") | ||
| 26 | file, _ := os.ReadFile("index.html") | ||
| 27 | w.Write(file) | ||
| 28 | }) | ||
| 29 | router.Route("/api", func(r chi.Router) { | ||
| 30 | r.Get("/view", view) | ||
| 31 | r.Get("/filesystem/*", h(app, filesystemGet)) | ||
| 32 | r.Post("/filesystem/*", h(app, filesystemPost)) | ||
| 33 | r.Delete("/filesystem/*", h(app, filesystemDelete)) | ||
| 34 | }) | ||
| 35 | |||
| 36 | if err := http.ListenAndServe("0.0.0.0:5000", router); err != nil { | ||
| 37 | panic(err) | ||
| 38 | } | ||
| 39 | } | ||
| 40 | |||
| 41 | func h(app *App, f func(*App, http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { | ||
| 42 | return func(w http.ResponseWriter, r *http.Request) { | ||
| 43 | f(app, w, r) | ||
| 44 | } | ||
| 45 | } | ||
| 46 | |||
| 47 | func view(w http.ResponseWriter, r *http.Request) { | ||
| 48 | w.Write([]byte("Hi")) | ||
| 49 | } | ||
| 50 | |||
| 51 | func filesystemGet(app *App, w http.ResponseWriter, r *http.Request) { | ||
| 52 | path, err := ParsePath("/" + chi.URLParam(r, "*")) | ||
| 53 | if err != nil { | ||
| 54 | panic(err) | ||
| 55 | } | ||
| 56 | |||
| 57 | // response := map[string]interface{}{ | ||
| 58 | // "path": path, | ||
| 59 | // "message": "Filesystem path requested", | ||
| 60 | // } | ||
| 61 | |||
| 62 | entry := app.Filesystem.GetEntry(path) | ||
| 63 | |||
| 64 | w.Header().Set("Content-Type", "application/json") | ||
| 65 | json.NewEncoder(w).Encode(entry) | ||
| 66 | } | ||
| 67 | |||
| 68 | func filesystemPost(app *App, w http.ResponseWriter, r *http.Request) { | ||
| 69 | if err := r.ParseMultipartForm(1024 * 1024); err != nil { | ||
| 70 | panic(err) | ||
| 71 | } | ||
| 72 | |||
| 73 | } | ||
| 74 | |||
| 75 | func filesystemDelete(app *App, w http.ResponseWriter, r *http.Request) {} | ||
diff --git a/operation.go b/operation.go new file mode 100644 index 0000000..80f7c04 --- /dev/null +++ b/operation.go | |||
| @@ -0,0 +1,147 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | "strconv" | ||
| 6 | "strings" | ||
| 7 | |||
| 8 | "github.com/pkg/errors" | ||
| 9 | ) | ||
| 10 | |||
| 11 | const ( | ||
| 12 | Operation_Type_CreateFile string = "create_file" | ||
| 13 | Operation_Type_CreateDir string = "create_dir" | ||
| 14 | Operation_Type_Remove string = "remove" | ||
| 15 | Operation_Type_Rename string = "rename" | ||
| 16 | ) | ||
| 17 | |||
| 18 | type Operation struct { | ||
| 19 | Timestamp uint64 | ||
| 20 | Revision uint64 | ||
| 21 | Email string | ||
| 22 | Type string | ||
| 23 | Params []string | ||
| 24 | } | ||
| 25 | |||
| 26 | type CreateFileParams struct { | ||
| 27 | Path string | ||
| 28 | Blob string | ||
| 29 | } | ||
| 30 | |||
| 31 | type CreateDirParams struct { | ||
| 32 | Path string | ||
| 33 | } | ||
| 34 | |||
| 35 | type RemoveParams struct { | ||
| 36 | Path string | ||
| 37 | } | ||
| 38 | |||
| 39 | type RenameParams struct { | ||
| 40 | Old string | ||
| 41 | New string | ||
| 42 | } | ||
| 43 | |||
| 44 | func (op *Operation) GetCreateFileParams() CreateFileParams { | ||
| 45 | op.assertType(Operation_Type_CreateFile) | ||
| 46 | op.assertParamsLen(2) | ||
| 47 | return CreateFileParams{ | ||
| 48 | Path: op.Params[0], | ||
| 49 | Blob: op.Params[1], | ||
| 50 | } | ||
| 51 | } | ||
| 52 | |||
| 53 | func (op *Operation) GetCreateDirParams() CreateDirParams { | ||
| 54 | op.assertType(Operation_Type_CreateDir) | ||
| 55 | op.assertParamsLen(1) | ||
| 56 | return CreateDirParams{ | ||
| 57 | Path: op.Params[0], | ||
| 58 | } | ||
| 59 | } | ||
| 60 | |||
| 61 | func (op *Operation) GetRemoveParams() RemoveParams { | ||
| 62 | op.assertType(Operation_Type_Remove) | ||
| 63 | op.assertParamsLen(1) | ||
| 64 | return RemoveParams{ | ||
| 65 | Path: op.Params[0], | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | func (op *Operation) GetRenameParams() RenameParams { | ||
| 70 | op.assertType(Operation_Type_Rename) | ||
| 71 | op.assertParamsLen(2) | ||
| 72 | return RenameParams{ | ||
| 73 | Old: op.Params[0], | ||
| 74 | New: op.Params[1], | ||
| 75 | } | ||
| 76 | } | ||
| 77 | |||
| 78 | func (op *Operation) assertType(t string) { | ||
| 79 | if op.Type != t { | ||
| 80 | panic(fmt.Sprintf("unexpected operation type, got %s but expected %s", op.Type, t)) | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | func (op *Operation) assertParamsLen(n int) { | ||
| 85 | if len(op.Params) != n { | ||
| 86 | panic(fmt.Sprintf("unexpected operation parameter length, got %d but expected %d", len(op.Params), n)) | ||
| 87 | } | ||
| 88 | } | ||
| 89 | |||
| 90 | func ParseOperation(line string) (*Operation, error) { | ||
| 91 | op := &Operation{} | ||
| 92 | baseErr := fmt.Errorf("invalid operation line: '%s'", line) | ||
| 93 | components := strings.Split(line, " ") | ||
| 94 | if len(components) < 4 { | ||
| 95 | return nil, errors.Wrapf(baseErr, "invalid number of components %d", len(components)) | ||
| 96 | } | ||
| 97 | |||
| 98 | if timestamp, err := strconv.ParseUint(components[0], 10, 64); err != nil { | ||
| 99 | return nil, errors.Wrapf(baseErr, "invalid timestamp %v: %v", components[0], err) | ||
| 100 | } else { | ||
| 101 | op.Timestamp = timestamp | ||
| 102 | } | ||
| 103 | |||
| 104 | if revision, err := strconv.ParseUint(components[0], 10, 64); err != nil { | ||
| 105 | return nil, errors.Wrapf(baseErr, "invalid revision %v: %v", components[0], err) | ||
| 106 | } else { | ||
| 107 | op.Revision = revision | ||
| 108 | } | ||
| 109 | |||
| 110 | op.Email = components[2] | ||
| 111 | op.Type = components[3] | ||
| 112 | op.Params = components[4:] | ||
| 113 | |||
| 114 | switch op.Type { | ||
| 115 | case Operation_Type_CreateFile: | ||
| 116 | if len(op.Params) != 2 { | ||
| 117 | return nil, errors.Wrapf(baseErr, "invalid number of parameters for operation create file") | ||
| 118 | } | ||
| 119 | case Operation_Type_CreateDir: | ||
| 120 | if len(op.Params) != 1 { | ||
| 121 | return nil, errors.Wrapf(baseErr, "invalid number of parameters for operation create dir") | ||
| 122 | } | ||
| 123 | |||
| 124 | case Operation_Type_Remove: | ||
| 125 | if len(op.Params) != 1 { | ||
| 126 | return nil, errors.Wrapf(baseErr, "invalid number of parameters for operation remove") | ||
| 127 | } | ||
| 128 | |||
| 129 | case Operation_Type_Rename: | ||
| 130 | if len(op.Params) != 2 { | ||
| 131 | return nil, errors.Wrapf(baseErr, "invalid number of parameters for operation rename") | ||
| 132 | } | ||
| 133 | default: | ||
| 134 | return nil, errors.Wrapf(baseErr, "invalid operation type '%v'", op.Type) | ||
| 135 | } | ||
| 136 | |||
| 137 | return op, nil | ||
| 138 | } | ||
| 139 | |||
| 140 | func (op *Operation) String() string { | ||
| 141 | params := strings.Join(op.Params, " ") | ||
| 142 | base := fmt.Sprintf("%d %d %s %s", op.Timestamp, op.Revision, op.Email, op.Type) | ||
| 143 | if len(params) > 0 { | ||
| 144 | base = base + " " + params | ||
| 145 | } | ||
| 146 | return base | ||
| 147 | } | ||
| @@ -0,0 +1,75 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | "strings" | ||
| 6 | ) | ||
| 7 | |||
| 8 | type PathComponent string | ||
| 9 | |||
| 10 | type Path []PathComponent | ||
| 11 | |||
| 12 | func NewPathComponent(s string) (PathComponent, error) { | ||
| 13 | if s != strings.TrimSpace(s) { | ||
| 14 | return PathComponent(""), fmt.Errorf("path component cannot contain leading or trailing spaces") | ||
| 15 | } | ||
| 16 | if strings.Contains(s, "/") { | ||
| 17 | return PathComponent(""), fmt.Errorf("path component cannot contain '/'") | ||
| 18 | } | ||
| 19 | if len(s) == 0 { | ||
| 20 | return PathComponent(""), fmt.Errorf("path component cannot be empty") | ||
| 21 | } | ||
| 22 | return PathComponent(s), nil | ||
| 23 | } | ||
| 24 | |||
| 25 | func (c PathComponent) String() string { | ||
| 26 | return string(c) | ||
| 27 | } | ||
| 28 | |||
| 29 | func (p Path) Components() []PathComponent { | ||
| 30 | return []PathComponent(p) | ||
| 31 | } | ||
| 32 | |||
| 33 | func (p Path) Parent() Path { | ||
| 34 | components := []PathComponent(p) | ||
| 35 | if len(components) == 0 { | ||
| 36 | return Path(components) | ||
| 37 | } else { | ||
| 38 | return Path(components[:len(components)-1]) | ||
| 39 | } | ||
| 40 | } | ||
| 41 | |||
| 42 | func (p Path) IsRoot() bool { | ||
| 43 | return len(p.Components()) == 0 | ||
| 44 | } | ||
| 45 | |||
| 46 | func ParsePath(pathStr string) (Path, error) { | ||
| 47 | if pathStr == "" || pathStr == "/" { | ||
| 48 | return Path{}, nil | ||
| 49 | } | ||
| 50 | |||
| 51 | if !strings.HasPrefix(pathStr, "/") { | ||
| 52 | return nil, fmt.Errorf("path must be absolute, got: %s", pathStr) | ||
| 53 | } | ||
| 54 | |||
| 55 | pathStr = strings.TrimPrefix(pathStr, "/") | ||
| 56 | if len(pathStr) == 0 { | ||
| 57 | return Path{}, nil | ||
| 58 | } | ||
| 59 | |||
| 60 | parts := strings.Split(pathStr, "/") | ||
| 61 | components := make([]PathComponent, 0, len(parts)) | ||
| 62 | |||
| 63 | for _, part := range parts { | ||
| 64 | if len(part) == 0 { | ||
| 65 | return nil, fmt.Errorf("empty path component in: %s", pathStr) | ||
| 66 | } | ||
| 67 | component, err := NewPathComponent(part) | ||
| 68 | if err != nil { | ||
| 69 | return nil, fmt.Errorf("invalid path component '%s': %w", part, err) | ||
| 70 | } | ||
| 71 | components = append(components, component) | ||
| 72 | } | ||
| 73 | |||
| 74 | return Path(components), nil | ||
| 75 | } | ||
diff --git a/service.yaml b/service.yaml new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/service.yaml | |||
