made filesever for fs and dbfs
sophuwu sophie@skisiel.com
Thu, 16 Jan 2025 03:27:19 +0100
4 files changed,
334 insertions(+),
131 deletions(-)
A
dbfs/dbfs.go
@@ -0,0 +1,122 @@
+package dbfs + +import ( + "fmt" + bolt "go.etcd.io/bbolt" + "path/filepath" + "sophuwu.site/cdn/fileserver" + "strings" +) + +type DirEntry = fileserver.DirEntry + +// DBFS represents a file system in a database +type DBFS struct { + db *bolt.DB +} + +// OpenDB opens or creates the database +func OpenDB(dbPath string) (*DBFS, error) { + db, err := bolt.Open(dbPath, 0600, nil) + if err != nil { + return nil, err + } + err = db.Update(func(tx *bolt.Tx) error { + _, err = tx.CreateBucketIfNotExists([]byte("root")) + return err + }) + return &DBFS{db: db}, err +} + +// GetEntry retrieves the content of a file or the list of items in a directory +func (fs *DBFS) GetEntry(path string) ([]byte, []DirEntry, error) { + path = strings.Trim(path, "/") + fullpath := path + paths := strings.Split(filepath.Dir(path), "/") + path = filepath.Base(path) + var items []DirEntry + var item DirEntry + var data []byte + return data, items, fs.db.View(func(tx *bolt.Tx) error { + currentBucket := tx.Bucket([]byte("root")) + if currentBucket == nil { + return fmt.Errorf("root bucket does not exist") + } + + for _, dir := range paths { + if dir == "" || dir == "." { + continue + } + currentBucket = currentBucket.Bucket([]byte(dir)) + if currentBucket == nil { + return fmt.Errorf("directory %s does not exist", dir) + } + } + if path != "" && path != "." { + data = currentBucket.Get([]byte(path)) + if data != nil { + return nil + } + currentBucket = currentBucket.Bucket([]byte(path)) + } + if currentBucket != nil { + return currentBucket.ForEach(func(k, v []byte) error { + if len(k) < 1 || strings.HasPrefix(string(k), ".") { + return nil + } + item = DirEntry{ + Name: string(k), + FullName: filepath.Join(fullpath, string(k)), + Size: len(v), + IsDir: false, + } + if item.Size == 0 && currentBucket.Bucket(k) != nil { + item.Name += "/" + item.IsDir = true + } + items = append(items, item) + return nil + }) + } + return fmt.Errorf("entry %s does not exist", path) + }) +} + +// PutFile stores the content of a file, creating directories as needed +func (fs *DBFS) PutFile(path string, data []byte) error { + path = strings.Trim(path, "/") + paths := strings.Split(filepath.Dir(path), "/") + path = filepath.Base(path) + if len(path) < 1 || strings.HasPrefix(path, ".") { + return fmt.Errorf("invalid file name") + } + var err error + return fs.db.Update(func(tx *bolt.Tx) error { + currentBucket := tx.Bucket([]byte("root")) + if currentBucket == nil { + return fmt.Errorf("root bucket does not exist") + } + + for _, dir := range paths { + if dir == "" || dir == "." { + continue + } + if currentBucket.Get([]byte(dir)) != nil { + return fmt.Errorf("directory %s is a file", dir) + } + currentBucket, err = currentBucket.CreateBucketIfNotExists([]byte(dir)) + if err != nil || currentBucket == nil { + return fmt.Errorf("directory %s does not exist", dir) + } + } + if currentBucket.Bucket([]byte(path)) != nil { + return fmt.Errorf("file %s is a directory", path) + } + return currentBucket.Put([]byte(path), data) + }) +} + +// Close the database +func (fs *DBFS) Close() error { + return fs.db.Close() +}
A
dir/dirent.go
@@ -0,0 +1,62 @@
+package dir + +import ( + "errors" + "io" + "io/fs" + "net/http" + "path/filepath" + "sophuwu.site/cdn/fileserver" +) + +func Open(path string) func(string) ([]byte, []fileserver.DirEntry, error) { + d := Dir{http.Dir(path)} + return d.GetEntry +} + +type Dir struct { + H http.FileSystem +} + +type DirEntry = fileserver.DirEntry + +func (d *Dir) GetEntry(path string) (data []byte, items []DirEntry, err error) { + var f http.File + f, err = d.H.Open(path) + if err != nil { + return + } + var fi fs.FileInfo + fi, err = f.Stat() + if err != nil { + return + } + if fi.IsDir() { + var de []fs.FileInfo + de, err = f.Readdir(0) + if err != nil { + return + } + items = []DirEntry{} + for _, d := range de { + items = append(items, DirEntry{ + Name: d.Name() + func() string { + if d.IsDir() { + return "/" + } + return "" + }(), + FullName: filepath.Join(path, d.Name()), + Size: int(d.Size()), + IsDir: d.IsDir(), + }) + } + return + } + if fi.Mode().IsRegular() { + data, err = io.ReadAll(f) + return + } + err = errors.New("not a regular file") + return +}
A
fileserver/fileserver.go
@@ -0,0 +1,117 @@
+package fileserver + +import ( + "fmt" + "html/template" + "io" + "net/http" + "path/filepath" + "strings" +) + +type DirEntry struct { + Name string + FullName string + Size int + IsDir bool +} + +func (d DirEntry) Si() string { + if d.IsDir { + return "" + } + f := float64(d.Size) + i := 0 + for f > 1024 { + f /= 1024 + i++ + } + s := fmt.Sprintf("%.2f", f) + s = strings.TrimRight(s, ".0") + return fmt.Sprintf("%s %cB", s, " KMGTPEZY"[i]) +} + +type TemplateData struct { + Path string + Dirs []DirEntry + Items []DirEntry +} + +var Temp *template.Template + +func FillTemp(w io.Writer, path string, items []DirEntry) error { + var data = TemplateData{Path: path, Dirs: []DirEntry{}, Items: []DirEntry{}} + for _, item := range items { + if item.IsDir { + data.Dirs = append(data.Dirs, item) + } else { + data.Items = append(data.Items, item) + } + } + return Temp.ExecuteTemplate(w, "index", data) +} + +func init() { + Temp = template.New("index") + Temp.Parse(`{{ define "index" }} +<!DOCTYPE html> +<html> +<head> +<title>{{ .Path }}</title> +<style> +:root {--bord: #ccc;--fg: #fff;} +body {width: calc(100% - 2ch);margin: auto auto auto auto ;max-width: 800px;background: #262833;color: var(--fg);font-family: sans-serif;} +.trees {width: 100%;display: flex;flex-direction: column;padding: 0;margin: auto auto;border: 1px solid var(--bord);border-radius: 1ch;overflow: hidden;} +.trees a {display: contents;text-decoration: none;} +.filelabel {padding: 8px;font-size: 1rem;width: auto;margin: 0;display: grid;grid-template-columns: auto auto;grid-gap: 1ch;justify-content: space-between;align-items: center;border-radius: 0;background: transparent;} +.trees > a > * {border-bottom: 1px solid var(--bord);background: #1c1e26;} +.trees > a > *:hover {background: #2c2e46;} +.trees > a:last-child > * {border-bottom: none;} +a {color: var(--fg);text-decoration: none;} +.filelabel > :last-child {text-align: right;} +</style> +</head> +<body> +<h1>Index of: {{.Path}}</h1> +<div class="trees"> +{{ range .Dirs }} +<a href="{{ .FullName }}"><div class="filelabel"><span>{{ .Name }}</span><span>{{ .Si }}</span></div></a> +{{ end }} +{{ range .Items }} +<a href="{{ .FullName }}"><div class="filelabel"><span>{{ .Name }}</span><span>{{ .Si }}</span></div></a> +{{ end }} +</div> +</body> +</html> +{{ end }}`) +} + +func Handler(prefix string, dir func(string) ([]byte, []DirEntry, error)) (string, http.Handler) { + return prefix, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + data, items, err := dir(strings.TrimPrefix(r.URL.Path, prefix)) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + w.WriteHeader(http.StatusOK) + if data != nil { + w.Write(data) + return + } + items = append([]DirEntry{{ + Name: "../", + FullName: filepath.Dir(strings.TrimSuffix(r.URL.Path, "/")), + Size: 0, + IsDir: true, + }}, items...) + + err = FillTemp(w, r.URL.Path, items) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) +} + +func Handle(prefix string, dir func(string) ([]byte, []DirEntry, error)) { + http.Handle(Handler(prefix, dir)) +}
M
main.go
→
main.go
@@ -1,10 +1,15 @@
package main import ( + "context" + "errors" "fmt" - bolt "go.etcd.io/bbolt" - "path/filepath" - "strings" + "net/http" + "os" + "os/signal" + "sophuwu.site/cdn/dbfs" + "sophuwu.site/cdn/dir" + "sophuwu.site/cdn/fileserver" ) // var db *bolt.DB@@ -17,143 +22,40 @@ // log.Fatalln(err)
// } // } -// FileSystem represents a file system in a database -type FileSystem struct { - db *bolt.DB -} - -// OpenDB opens or creates the database -func OpenDB(dbPath string) (*FileSystem, error) { - db, err := bolt.Open(dbPath, 0600, nil) +func main() { + db, err := dbfs.OpenDB("build/my.db") if err != nil { - return nil, err + fmt.Println(err) + return } - err = db.Update(func(tx *bolt.Tx) error { - _, err = tx.CreateBucketIfNotExists([]byte("root")) - return err - }) - return &FileSystem{db: db}, err -} -type DirEntry struct { - Name string - Size int - IsDir bool -} - -// GetEntry retrieves the content of a file or the list of items in a directory -func (fs *FileSystem) GetEntry(path string) ([]byte, []DirEntry, error) { - path = strings.Trim(path, "/") - paths := strings.Split(filepath.Dir(path), "/") - path = filepath.Base(path) - var items []DirEntry - var item DirEntry - var data []byte - return data, items, fs.db.View(func(tx *bolt.Tx) error { - currentBucket := tx.Bucket([]byte("root")) - if currentBucket == nil { - return fmt.Errorf("root bucket does not exist") - } + fileserver.Handle("/dir/", dir.Open(".")) + fileserver.Handle("/db/", db.GetEntry) - for _, dir := range paths { - if dir == "" || dir == "." { - continue - } - currentBucket = currentBucket.Bucket([]byte(dir)) - if currentBucket == nil { - return fmt.Errorf("directory %s does not exist", dir) - } - } - if path != "" && path != "." { - data = currentBucket.Get([]byte(path)) - if data != nil { - return nil - } - currentBucket = currentBucket.Bucket([]byte(path)) - } - if currentBucket != nil { - return currentBucket.ForEach(func(k, v []byte) error { - item = DirEntry{ - Name: string(k), - Size: len(v), - IsDir: false, - } - if item.Size == 0 && currentBucket.Bucket(k) != nil { - item.Name += "/" - item.IsDir = true - } - items = append(items, item) - return nil - }) - } - return fmt.Errorf("entry %s does not exist", path) - }) -} + sig := make(chan os.Signal) + signal.Notify(sig, os.Interrupt) -// PutFile stores the content of a file, creating directories as needed -func (fs *FileSystem) PutFile(path string, data []byte) error { - path = strings.Trim(path, "/") - paths := strings.Split(filepath.Dir(path), "/") - path = filepath.Base(path) - if len(path) < 1 || strings.HasPrefix(path, ".") { - return fmt.Errorf("invalid file name") + server := http.Server{ + Addr: ":8080", + Handler: nil, } - var err error - return fs.db.Update(func(tx *bolt.Tx) error { - currentBucket := tx.Bucket([]byte("root")) - if currentBucket == nil { - return fmt.Errorf("root bucket does not exist") - } - for _, dir := range paths { - if dir == "" || dir == "." { - continue - } - if currentBucket.Get([]byte(dir)) != nil { - return fmt.Errorf("directory %s is a file", dir) - } - currentBucket, err = currentBucket.CreateBucketIfNotExists([]byte(dir)) - if err != nil || currentBucket == nil { - return fmt.Errorf("directory %s does not exist", dir) - } - } - if currentBucket.Bucket([]byte(path)) != nil { - return fmt.Errorf("file %s is a directory", path) + go func() { + err = server.ListenAndServe() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + fmt.Println(err) } - return currentBucket.Put([]byte(path), data) - }) -} - -// Close the database -func (fs *FileSystem) Close() error { - return fs.db.Close() -} - -func main() { - fs, err := OpenDB("build/my.db") + }() + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt) + <-quit + fmt.Println("Closing databases") + err = db.Close() if err != nil { fmt.Println(err) - return } - defer fs.Close() - - // err = fs.PutFile("index.txt", []byte("Hello")) - // if err != nil { - // fmt.Println(err) - // return - // } - - data, items, err := fs.GetEntry("/") - if err != nil { - fmt.Println(err) - return - } - if data != nil { - fmt.Println(string(data)) - return - } - for _, item := range items { - fmt.Println(item) - } - + fmt.Println("Closed databases") + fmt.Println("Stopping server") + _ = server.Shutdown(context.Background()) + fmt.Println("Server stopped") }