summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2025-08-07 13:49:25 +0100
committerdiogo464 <[email protected]>2025-08-07 13:49:25 +0100
commit9a25abd1d6ef6f5b0e2c08751183f63db43c73a5 (patch)
treeda4c902ee86df1c0c4b875bc1697dd8b08590123
init
-rwxr-xr-xauth.sh8
-rw-r--r--deployment.yaml28
-rw-r--r--filesystem.go244
-rw-r--r--go.mod8
-rw-r--r--go.sum4
-rw-r--r--index.html17
-rw-r--r--main.go75
-rw-r--r--operation.go147
-rw-r--r--path.go75
-rw-r--r--service.yaml0
10 files changed, 606 insertions, 0 deletions
diff --git a/auth.sh b/auth.sh
new file mode 100755
index 0000000..aa3dc1f
--- /dev/null
+++ b/auth.sh
@@ -0,0 +1,8 @@
1#!/usr/bin/env bash
2
3podman 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---
2apiVersion: apps/v1
3kind: Deployment
4metadata:
5 name: auth
6spec:
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 @@
1package main
2
3import "fmt"
4
5type Filesystem_Ent_Type string
6
7const (
8 Filesystem_Ent_Type_Dir Filesystem_Ent_Type = "dir"
9 Filesystem_Ent_Type_File Filesystem_Ent_Type = "file"
10)
11
12type Filesystem struct {
13 Root *Filesystem_Ent
14}
15
16func 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
25type Filesystem_Ent struct {
26 Type Filesystem_Ent_Type
27 Children map[PathComponent]*Filesystem_Ent
28 Blob string
29}
30
31func (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
42func (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
73func (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
88func (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
123func (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
166func (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
195func (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}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..54ccc54
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,8 @@
1module git.d464.sh/fctdrive
2
3go 1.24.5
4
5require (
6 github.com/go-chi/chi/v5 v5.2.2
7 github.com/pkg/errors v0.9.1
8)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..4085610
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,4 @@
1github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
2github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
3github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
4github.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>
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..1be1067
--- /dev/null
+++ b/main.go
@@ -0,0 +1,75 @@
1package main
2
3import (
4 "encoding/json"
5 "net/http"
6 "os"
7
8 "github.com/go-chi/chi/v5"
9)
10
11type App struct {
12 Operations []*Operation
13 Filesystem *Filesystem
14}
15
16func 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
41func 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
47func view(w http.ResponseWriter, r *http.Request) {
48 w.Write([]byte("Hi"))
49}
50
51func 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
68func 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
75func 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 @@
1package main
2
3import (
4 "fmt"
5 "strconv"
6 "strings"
7
8 "github.com/pkg/errors"
9)
10
11const (
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
18type Operation struct {
19 Timestamp uint64
20 Revision uint64
21 Email string
22 Type string
23 Params []string
24}
25
26type CreateFileParams struct {
27 Path string
28 Blob string
29}
30
31type CreateDirParams struct {
32 Path string
33}
34
35type RemoveParams struct {
36 Path string
37}
38
39type RenameParams struct {
40 Old string
41 New string
42}
43
44func (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
53func (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
61func (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
69func (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
78func (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
84func (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
90func 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
140func (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}
diff --git a/path.go b/path.go
new file mode 100644
index 0000000..bb81229
--- /dev/null
+++ b/path.go
@@ -0,0 +1,75 @@
1package main
2
3import (
4 "fmt"
5 "strings"
6)
7
8type PathComponent string
9
10type Path []PathComponent
11
12func 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
25func (c PathComponent) String() string {
26 return string(c)
27}
28
29func (p Path) Components() []PathComponent {
30 return []PathComponent(p)
31}
32
33func (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
42func (p Path) IsRoot() bool {
43 return len(p.Components()) == 0
44}
45
46func 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