aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorVikas Kushwaha <dev@vikas.rocks>2024-11-21 13:54:38 +0530
committerVikas Kushwaha <dev@vikas.rocks>2024-11-21 13:54:38 +0530
commit6a16bbdcdb40406592e47ee8d489f857837e5c96 (patch)
tree8d1a9f72115a106657e059f56e5b47df1a92f483 /src
Initial commit
Diffstat (limited to 'src')
-rw-r--r--src/copy.go124
-rw-r--r--src/files.go449
-rw-r--r--src/go.mod5
-rw-r--r--src/go.sum2
-rw-r--r--src/helpers.go292
-rw-r--r--src/server.go298
-rw-r--r--src/static/icons/bs/actions/arrow-left-short.svg3
-rw-r--r--src/static/icons/bs/actions/arrow-left.svg3
-rw-r--r--src/static/icons/bs/actions/arrow-right-short.svg3
-rw-r--r--src/static/icons/bs/actions/arrow-right.svg3
-rw-r--r--src/static/icons/bs/actions/backspace.svg4
-rw-r--r--src/static/icons/bs/actions/check.svg3
-rw-r--r--src/static/icons/bs/actions/clipboard.svg4
-rw-r--r--src/static/icons/bs/actions/copy.svg3
-rw-r--r--src/static/icons/bs/actions/download.svg4
-rw-r--r--src/static/icons/bs/actions/folder-plus.svg4
-rw-r--r--src/static/icons/bs/actions/folder.svg3
-rw-r--r--src/static/icons/bs/actions/scissors.svg3
-rw-r--r--src/static/icons/bs/actions/trash.svg4
-rw-r--r--src/static/icons/bs/actions/upload.svg4
-rw-r--r--src/static/icons/bs/files/file-earmark.svg3
-rw-r--r--src/static/icons/bs/files/folder-symlink.svg4
-rw-r--r--src/static/icons/bs/files/folder2.svg3
-rw-r--r--src/static/icons/bs/files/link-45deg.svg4
-rw-r--r--src/static/icons/bs/files/link-broken-45deg.svg4
-rw-r--r--src/static/icons/bs/files/question.svg3
-rw-r--r--src/static/icons/bs/hdd.svg4
-rw-r--r--src/static/icons/bs/house-door.svg3
-rw-r--r--src/static/icons/bs/person-circle.svg4
-rw-r--r--src/static/script.js41
-rw-r--r--src/static/style.css356
-rw-r--r--src/templates.go45
-rw-r--r--src/templates/base.html17
-rw-r--r--src/templates/profile.html24
-rw-r--r--src/templates/viewDir.html106
-rw-r--r--src/templates/viewHome.html29
36 files changed, 1868 insertions, 0 deletions
diff --git a/src/copy.go b/src/copy.go
new file mode 100644
index 0000000..1076d41
--- /dev/null
+++ b/src/copy.go
@@ -0,0 +1,124 @@
+
+// Package gorecurcopy provides recursive copying in Go (golang) with a
+// minimum of extra packages. Original concept by Oleg Neumyvakin
+// (https://stackoverflow.com/users/1592008/oleg-neumyvakin) and modified
+// by Dirk Avery.
+// Slightly modified by me.
+package main
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ // "syscall"
+)
+
+// copyDir recursively copies a src directory to a destination.
+func copyDir(src, dst string) error {
+ entries, err := ioutil.ReadDir(src)
+ if err != nil {
+ return err
+ }
+ for _, entry := range entries {
+ sourcePath := filepath.Join(src, entry.Name())
+ destPath := filepath.Join(dst, entry.Name())
+
+ fileInfo, err := os.Lstat(sourcePath)
+ if err != nil {
+ return err
+ }
+
+ switch fileInfo.Mode() & os.ModeType {
+ case os.ModeDir:
+ if err := _createDir(destPath, 0755); err != nil {
+ return err
+ }
+ if err := copyDir(sourcePath, destPath); err != nil {
+ return err
+ }
+ case os.ModeSymlink:
+ if err := copySymlink(sourcePath, destPath); err != nil {
+ return err
+ }
+ default:
+ if err := _copy(sourcePath, destPath); err != nil {
+ return err
+ }
+ }
+
+ /*
+ stat, ok := fileInfo.Sys().(*syscall.Stat_t)
+ if !ok {
+ return fmt.Errorf("failed to get raw syscall.Stat_t data for '%s'", sourcePath)
+ }
+ if err := os.Lchown(destPath, int(stat.Uid), int(stat.Gid)); err != nil {
+ return err
+ }
+ */
+
+ isSymlink := entry.Mode()&os.ModeSymlink != 0
+ if !isSymlink {
+ if err := os.Chmod(destPath, entry.Mode()); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+// Copy copies a src file to a dst file where src and dst are regular files.
+func _copy(src, dst string) error {
+ sourceFileStat, err := os.Stat(src)
+ if err != nil {
+ return err
+ }
+
+ if !sourceFileStat.Mode().IsRegular() {
+ return fmt.Errorf("%s is not a regular file", src)
+ }
+
+ source, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer source.Close()
+
+ destination, err := os.Create(dst)
+ if err != nil {
+ return err
+ }
+ defer destination.Close()
+ _, err = io.Copy(destination, source)
+ return err
+}
+
+func _exists(path string) bool {
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ return false
+ }
+
+ return true
+}
+
+func _createDir(dir string, perm os.FileMode) error {
+ if _exists(dir) {
+ return nil
+ }
+
+ if err := os.MkdirAll(dir, perm); err != nil {
+ return fmt.Errorf("failed to create directory: '%s', error: '%s'", dir, err.Error())
+ }
+
+ return nil
+}
+
+// copySymlink copies a symbolic link from src to dst.
+func copySymlink(src, dst string) error {
+ link, err := os.Readlink(src)
+ if err != nil {
+ return err
+ }
+ return os.Symlink(link, dst)
+}
diff --git a/src/files.go b/src/files.go
new file mode 100644
index 0000000..831ed4d
--- /dev/null
+++ b/src/files.go
@@ -0,0 +1,449 @@
+package main
+
+import (
+ "archive/zip"
+ "bufio"
+ "io"
+ "os"
+ "fmt"
+ "html/template"
+ "net/http"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+ // "syscall"
+)
+
+type MalformedLinkError struct {
+ Link string
+ Target string
+}
+
+func (e *MalformedLinkError) Error() string { return fmt.Sprintf("%s: broken link to %s", e.Link, e.Target) }
+
+type FileNode struct {
+ URI string
+ Path string
+ IsDir bool
+ Info os.FileInfo
+ Data any
+}
+
+func (fileNode *FileNode) HTMLPath() template.HTML {
+ var htmlpath string
+ htmlpath += `<a href="/view/` + `">` + "Home" + `</a> `
+ p := strings.Split(fileNode.URI, string(os.PathSeparator))
+ for i, dir := range p {
+ if p[i] != "" {
+ htmlpath += `> <a href="/view/` + filepath.Join(p[:i+1]...) + `">` + dir + `</a> `
+ }
+ }
+ return template.HTML(htmlpath)
+}
+
+func (fileNode *FileNode) EvalSymlinks() (string, *FileNode, error) {
+ var err error
+ target, path, err := linkDeref(fileNode.Path);
+ if err != nil {
+ if os.IsNotExist(err) {
+ return "", nil, err
+ }
+ return target, nil, err
+ }
+ fileInfo, err := os.Stat(path)
+ if err != nil {
+ return target, nil, err
+ }
+ return target, &FileNode{
+ Path: path,
+ URI: strings.TrimPrefix(path, homeDir),
+ Info: fileInfo,
+ IsDir: fileInfo.IsDir(),
+ }, nil
+}
+
+func (fileNode *FileNode) IconPath() (string, error) {
+ var icon string;
+ switch fileNode.Info.Mode() & os.ModeType {
+ default: icon = "file-earmark.svg"
+ case os.ModeIrregular: icon = "question.svg"
+ case os.ModeDir: icon = "folder2.svg"
+ case os.ModeSymlink:
+ _, fileNode, err := fileNode.EvalSymlinks()
+ if err != nil {
+ if !os.IsNotExist(err) {
+ return "", err
+ }
+ icon = "link-broken-45deg.svg"
+ } else {
+ if fileNode.IsDir {
+ icon = "folder-symlink.svg"
+ } else {
+ icon = "link-45deg.svg"
+ }
+ }
+ }
+ return filepath.Join("/static/icons/bs/files", icon), nil
+}
+
+func (fileNode *FileNode) Size() (string, error) {
+ var err error
+ if fileNode.Mode() == "l" {
+ _, fileNode, err = fileNode.EvalSymlinks()
+ if err != nil {
+ return "", nil
+ }
+ }
+ if fileNode.IsDir {
+ return "", nil
+ }
+ size := float64(fileNode.Info.Size())
+ if size < 100 {
+ return strconv.FormatFloat(size, 'f', 0, 64) + " B", nil
+ }
+ units := []string{" KB", " MB", " GB", " TB", " PB", " EB", " ZB"}
+ for i := 0; i < 7; i++ {
+ size /= 1024
+ if size < 100 {
+ return strconv.FormatFloat(size, 'f', 1, 64) + units[i], nil
+ }
+ }
+ return strconv.FormatFloat(size, 'f', 1, 64) + " YiB", nil
+}
+
+func (fileNode *FileNode) Mode() string {
+ switch fileNode.Info.Mode() & os.ModeType {
+ default: return "f"
+ case os.ModeDir: return "d"
+ case os.ModeSymlink: return "l"
+ }
+}
+
+func (fileNode *FileNode) ModDate() string {
+ t := fileNode.Info.ModTime()
+ return fmt.Sprintf("%.3s %d, %d\n", t.Month(), t.Day(), t.Year())
+}
+
+func (fileNode *FileNode) ModTime() string {
+ t := fileNode.Info.ModTime()
+ return fmt.Sprintf("%d:%d\n", t.Hour(), t.Minute())
+}
+
+
+func (fileNode *FileNode) Details() (string, error) {
+ text := "Non-Regular File"
+ switch fileNode.Info.Mode() & os.ModeType {
+
+ default:
+ if fileNode.Info.Size() == 0 {
+ return "Empty File", nil
+ }
+ file, err := os.Open(fileNode.Path)
+ if err != nil {
+ return "", err
+ }
+ buffer := make([]byte, 512)
+ _, err = file.Read(buffer)
+ if err != nil {
+ return "", err
+ }
+ contentType := http.DetectContentType(buffer)
+ if contentType == "application/octet-stream" {
+ text = "Text File"
+ } else {
+ text = "*"+contentType
+ }
+
+ case os.ModeDir:
+ text = "Folder"
+
+ case os.ModeSymlink:
+ target, _, err := fileNode.EvalSymlinks()
+ if err != nil {
+ if !os.IsNotExist(err) {
+ return "", err
+ }
+ if len(target) > 0 {
+ text = "Broken Link to '"+target+"'"
+ } else {
+ text = "Inaccessible Link"
+ }
+ } else {
+ text = "Link to " + target
+ }
+
+ case os.ModeSocket:
+ text = "Unix Socket"
+
+ case os.ModeDevice:
+ text = "Device File"
+
+ case os.ModeNamedPipe:
+ text = "Named Pipe"
+
+ case os.ModeTemporary:
+ text = "Temporary File"
+
+ case os.ModeAppend:
+ case os.ModeExclusive:
+ case os.ModeSetuid:
+ case os.ModeSetgid:
+ case os.ModeCharDevice:
+ case os.ModeSticky:
+ case os.ModeIrregular:
+ }
+
+ return text, nil
+}
+
+
+func getDirSize(path string) (int64, error) {
+ var size int64
+ err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if !info.IsDir() {
+ size += info.Size()
+ }
+ return err
+ })
+ return size, err
+}
+
+
+func getDirList(path string, sortBy string, ascending bool, dirsFirst bool) ([]*FileNode, error) {
+ entries, err := os.ReadDir(path)
+ if err != nil {
+ return nil, err
+ }
+ files := make([]*FileNode, len(entries))
+ for i, entry := range entries {
+ filePath := filepath.Join(path, entry.Name())
+ fileURI := strings.TrimLeft(path, homeDir)
+ fileInfo, err := entry.Info()
+ if err != nil {
+ return nil, err
+ }
+ files[i] = &FileNode{
+ Path: filePath,
+ URI: fileURI,
+ IsDir: entry.IsDir(),
+ Info: fileInfo,
+ }
+ }
+
+ switch sortBy {
+ case "name": sort.SliceStable(files, func(i, j int) bool {
+ return strings.ToLower(files[i].Info.Name()) < strings.ToLower(files[j].Info.Name())
+ })
+ case "size": sort.SliceStable(files, func(i, j int) bool {
+ return files[i].Info.Size() < files[j].Info.Size()
+ })
+ case "time": sort.SliceStable(files, func(i, j int) bool {
+ return files[i].Info.ModTime().Before(files[j].Info.ModTime())
+ })
+ }
+
+ if !ascending {
+ for i, j := 0, len(files)-1; i < j; i, j = i+1, j-1 {
+ files[i], files[j] = files[j], files[i]
+ }
+ }
+
+ if dirsFirst {
+ var dirs, notDirs []*FileNode
+ for _, fileNode := range files {
+ info, err := os.Stat(fileNode.Path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ info, err = os.Lstat(fileNode.Path)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ return nil, err
+ }
+ }
+ if info.IsDir() {
+ dirs = append(dirs, fileNode)
+ } else {
+ notDirs = append(notDirs, fileNode)
+ }
+ }
+ return append(dirs, notDirs...), nil
+ }
+
+ return files, nil
+}
+
+
+func addToZip(source string, writer *zip.Writer) error {
+ return filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ header, err := zip.FileInfoHeader(info)
+ if err != nil {
+ return err
+ }
+ header.Method = zip.Deflate
+ header.Name, err = filepath.Rel(filepath.Dir(source), path)
+ if err != nil {
+ return err
+ }
+ if info.IsDir() {
+ header.Name += "/"
+ }
+ headerWriter, err := writer.CreateHeader(header)
+ if err != nil {
+ return err
+ }
+ if !info.Mode().IsRegular() {
+ return nil
+ }
+ f, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ _, err = io.Copy(headerWriter, f)
+ return err
+ })
+}
+
+
+func readBuffer(path string) ([]string, error) {
+ buff, err := os.OpenFile(path, os.O_RDONLY|os.O_CREATE, 0600)
+ if err != nil {
+ return nil, err
+ }
+ defer buff.Close()
+
+ var buffer []string
+ scanner := bufio.NewScanner(buff)
+ for scanner.Scan() {
+ buffer = append(buffer, scanner.Text())
+ }
+ return buffer, nil
+}
+
+
+func fileExists(path string) (bool, error) {
+ _, err := os.Lstat(path)
+ if err == nil {
+ return true, nil
+ }
+ if os.IsNotExist(err) {
+ return false, nil
+ }
+ return false, err
+}
+
+
+func copyFile(src, dst string) error {
+ fin, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer fin.Close()
+
+ fout, err := os.Create(dst)
+ if err != nil {
+ return err
+ }
+ defer fout.Close()
+
+ _, err = io.Copy(fout, fin)
+ if err != nil {
+ return err
+ }
+ fin.Close()
+
+ return nil
+}
+
+
+func copyTo(src, dstDir string) error {
+ info, err := os.Lstat(src)
+ if err != nil {
+ return err
+ }
+ dst := filepath.Join(dstDir, info.Name())
+
+ fmt.Printf("Copying %s to %s\n", src, dstDir)
+ switch info.Mode() & os.ModeType {
+ case os.ModeDir:
+ if err := os.MkdirAll(dst, 0755); err != nil {
+ return err
+ }
+ if err := copyDir(src, dst); err != nil {
+ return err
+ }
+ case os.ModeSymlink:
+ if err := copySymlink(src, dst); err != nil {
+ return err
+ }
+ default:
+ if err := copyFile(src, dst); err != nil {
+ return err
+ }
+ }
+ fmt.Println("Finished Copying.\n\n")
+
+ if info.Mode()&os.ModeSymlink == 0 {
+ return os.Chmod(dst, info.Mode())
+ }
+ return nil
+}
+
+
+func linkDeref(link string) (string, string, error) {
+ target, err := os.Readlink(link)
+ if err != nil {
+ return "", "", err
+ }
+ path := target
+ if filepath.IsAbs(target) {
+ if !strings.HasPrefix(path, homeDir) {
+ return target, "", os.ErrNotExist
+ }
+ target = strings.TrimPrefix(target, homeDir)
+ } else {
+ path = filepath.Join(filepath.Dir(link), path)
+ if !strings.HasPrefix(path, homeDir) {
+ return target, "", os.ErrNotExist
+ }
+ }
+ return target, path, nil
+}
+
+
+func readData(key string) ([]byte, error) {
+ data, err := os.ReadFile(filepath.Join(dataDir, key))
+ if err != nil {
+ return nil, err
+ }
+ return data, nil
+}
+
+
+func writeData(key string, data []byte) error {
+ return os.WriteFile(filepath.Join(dataDir, key), data, 644)
+}
+
+
+var dataDir string
+
+func init() {
+ userHome, err := os.UserHomeDir()
+ if err != nil {
+ panic(err)
+ }
+ dataDir = filepath.Join(userHome, ".local/share/cloud-maker")
+ if err = os.MkdirAll(dataDir, 0755); err != nil {
+ panic(err)
+ }
+}
diff --git a/src/go.mod b/src/go.mod
new file mode 100644
index 0000000..106b070
--- /dev/null
+++ b/src/go.mod
@@ -0,0 +1,5 @@
+module cloud-maker/server
+
+go 1.23
+
+require golang.org/x/crypto v0.28.0 // indirect
diff --git a/src/go.sum b/src/go.sum
new file mode 100644
index 0000000..29e53ba
--- /dev/null
+++ b/src/go.sum
@@ -0,0 +1,2 @@
+golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
+golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
diff --git a/src/helpers.go b/src/helpers.go
new file mode 100644
index 0000000..c544d71
--- /dev/null
+++ b/src/helpers.go
@@ -0,0 +1,292 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
+ "os/user"
+ "path/filepath"
+ "regexp"
+ "strings"
+)
+
+type ServerError struct {
+ Err error
+ Message string
+ Status int
+}
+
+func (e *ServerError) Error() string { return e.Err.Error() }
+func (e *ServerError) Unwrap() error { return e.Err }
+
+
+func getFileNode(URL string) (*FileNode, *ServerError) {
+ path, err := url.PathUnescape(URL)
+ if err != nil {
+ return nil, &ServerError{err, "", 500}
+ }
+ p := strings.Split(path, "/")
+ fileURI := strings.Trim(strings.Join(p[2:], "/"), "/")
+ filePath := filepath.Join(homeDir, fileURI)
+
+ fileInfo, err := os.Lstat(filePath)
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, &ServerError{err, fileURI+" not found", 404}
+ }
+ return nil, &ServerError{err, "", 500}
+ }
+
+ return &FileNode{
+ Path: filePath,
+ URI: fileURI,
+ IsDir: fileInfo.IsDir(),
+ Info: fileInfo,
+ }, nil
+}
+
+
+var filePattern = regexp.MustCompile(`^-file-entry--(.+)$`)
+
+func getSelectedNodes(r *http.Request) (*FileNode, []*FileNode, *ServerError) {
+ fileNode, e := getFileNode(r.URL.Path)
+ if e != nil {
+ return nil, nil, e
+ }
+ r.ParseMultipartForm(65536)
+ fmt.Println(r.Form)
+ var fileNames []string
+ for key := range r.Form {
+ if match := filePattern.FindStringSubmatch(key); len(match) > 1 {
+ fileNames = append(fileNames, match[1])
+ }
+ }
+ fmt.Printf("FileNames: %s\n", fileNames)
+ if len(fileNames) == 0 {
+ return fileNode, []*FileNode{fileNode}, nil
+ }
+
+ files := make([]*FileNode, len(fileNames))
+ for i, fileName := range fileNames {
+ fileNode, e := getFileNode(filepath.Join(r.URL.Path, fileName))
+ if e != nil {
+ return fileNode, nil, e
+ }
+ files[i] = fileNode
+ }
+ return fileNode, files, nil
+}
+
+
+func sendFile(w http.ResponseWriter, r *http.Request, info ...string) {
+ fmt.Printf("info: %s\n", info[:])
+ if len(info) < 2 {
+ info = append(info, filepath.Base(info[0]))
+ }
+ w.Header().Set("Content-Disposition", "attachment; filename=" + info[1])
+ http.ServeFile(w, r, info[0])
+}
+
+
+func addSelectionToBuffer(w http.ResponseWriter, r *http.Request, bufferPath string) *ServerError {
+ _, files, e := getSelectedNodes(r)
+ if e != nil {
+ return e
+ }
+ buffer, err := readBuffer(bufferPath)
+ if err != nil {
+ return &ServerError{err, "", 500}
+ }
+ buff, err := os.OpenFile(bufferPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
+ if err != nil {
+ return &ServerError{err, "", 500}
+ }
+ var fileURI string
+
+ writeToBuffer:
+ for _, file := range files {
+ fileURI = strings.Trim(file.URI, "/")
+ for _, line := range buffer {
+ if strings.Trim(line, "/") == fileURI {
+ continue writeToBuffer
+ }
+ }
+ if file.IsDir {
+ fileURI += "/"
+ }
+ buff.WriteString("/" + fileURI + "\r\n")
+ }
+
+ http.Redirect(w, r, r.URL.Path, 303)
+ return nil
+}
+
+
+func deleteBuffer(w http.ResponseWriter, r *http.Request, bufferPath string) *ServerError {
+ err := os.Remove(bufferPath)
+ if err != nil {
+ return &ServerError{err, "", 500}
+ }
+ http.Redirect(w, r, r.URL.Path, 303)
+ return nil
+}
+
+
+func moveFilesFromBuffer(w http.ResponseWriter, r *http.Request, bufferPath string) *ServerError {
+ fileNode, serr := getFileNode(r.URL.Path)
+ if serr != nil {
+ return serr
+ }
+ if !fileNode.IsDir {
+ return &ServerError{nil, "Cannot move file, destination is not a directory", 400}
+ }
+ buffer, err := readBuffer(bufferPath)
+ if err != nil {
+ return &ServerError{err, "", 500}
+ }
+ for _, line := range buffer {
+ err := copyTo(filepath.Join(homeDir, line), fileNode.Path)
+ if err != nil {
+ return &ServerError{err, "", 500}
+ }
+ }
+ deleteBuffer(w, r, bufferPath)
+ return nil
+}
+
+
+func pasteFilesFromBuffer(w http.ResponseWriter, r *http.Request, bufferPath string) *ServerError {
+ fileNode, serr := getFileNode(r.URL.Path)
+ if serr != nil {
+ return serr
+ }
+ if !fileNode.IsDir {
+ return &ServerError{nil, "Cannot copy files, destination is not a directory", 400}
+ }
+ buffer, err := readBuffer(bufferPath)
+ if err != nil {
+ return &ServerError{err, "", 500}
+ }
+ fmt.Printf("Buffer content: %s\n", buffer)
+ for _, line := range buffer {
+ err := copyTo(filepath.Join(homeDir, line), fileNode.Path)
+ if err != nil {
+ return &ServerError{err, "", 500}
+ }
+ }
+ deleteBuffer(w, r, bufferPath)
+ return nil
+}
+
+
+func createNewDirectory(w http.ResponseWriter, r *http.Request) *ServerError {
+ fileNode, serr := getFileNode(r.URL.Path)
+ if serr != nil {
+ return serr
+ }
+ dirname := strings.TrimSpace(r.FormValue("newdir"))
+ msg := "Requested to create directory '"+dirname+"' in "+fileNode.URI+"\n"
+ if dirname == "" {
+ msg = "Directory name cannot be empty.\n" + msg
+ return &ServerError{nil, msg, 400}
+ }
+ if !fileNode.IsDir {
+ msg = "Cannot create directory, the given destination is a file.\n" + msg
+ return &ServerError{nil, msg, 400}
+ }
+ path := filepath.Join(fileNode.Path, dirname)
+ isExist, err := fileExists(path)
+ if isExist {
+ msg = "Cannot create directory, a file with given name already exists.\n" + msg
+ return &ServerError{nil, msg, 400}
+ }
+ err = os.Mkdir(path, 0755)
+ if err != nil {
+ return &ServerError{err, "", 500}
+ }
+ http.Redirect(w, r, r.URL.Path, 303)
+ return nil
+}
+
+
+func blockAction(w http.ResponseWriter, r *http.Request, action string) *ServerError {
+ msg := ""
+ _, files, serr := getSelectedNodes(r)
+ if serr != nil {
+ return serr
+ }
+ msg += "The " + action + " operation is currently disabled for testing and security reasons.\n"
+ msg += "You requested to " + action + " following files :-\n\n"
+ for _, file := range files {
+ msg += file.Path + "\n"
+ }
+ fmt.Fprintf(w, msg)
+ return nil
+}
+
+
+func deleteSelectedFiles(w http.ResponseWriter, r *http.Request) *ServerError {
+ // return blockAction(w, r, "delete")
+ fileNode, files, serr := getSelectedNodes(r)
+ if serr != nil {
+ return serr
+ }
+ for _, file := range files {
+ if file.Path == homeDir {
+ return &ServerError{nil, "Cannot delete root directory.", 400}
+ }
+ fmt.Printf("Deleting: %s\n", file.Path)
+ err := os.RemoveAll(file.Path)
+ if err != nil {
+ return &ServerError{err, "", 500}
+ }
+ fmt.Println("Deleted.")
+ }
+ isExist, err := fileExists(fileNode.Path)
+ if err != nil {
+ return &ServerError{err, "", 500}
+ }
+ if !isExist {
+ http.Redirect(w, r, "/view/" + filepath.Dir(fileNode.URI), 303)
+ } else {
+ http.Redirect(w, r, "/view/" + fileNode.URI, 303)
+ }
+ return nil
+}
+
+
+var (
+ homeDir = "/media/"
+ tempDir = "/tmp/cloud/"
+ cutBuffer = filepath.Join(tempDir, "cut_buffer")
+ copyBuffer = filepath.Join(tempDir, "copy_buffer")
+)
+
+
+func init() {
+ err := os.MkdirAll(tempDir, 0755);
+ if err != nil {
+ panic(err)
+ }
+ mountpoint := os.Getenv("CLOUD_MAKER_HOME")
+ if mountpoint != "" {
+ isExist, err := fileExists(mountpoint)
+ if err != nil {
+ panic(err)
+ }
+ if isExist {
+ homeDir = mountpoint
+ }
+ return
+ }
+
+ u, err := user.Current()
+ if err != nil {
+ panic(err)
+ }
+ homeDir += u.Username
+}
+
+
diff --git a/src/server.go b/src/server.go
new file mode 100644
index 0000000..16df50f
--- /dev/null
+++ b/src/server.go
@@ -0,0 +1,298 @@
+package main
+
+import (
+ "archive/zip"
+ "bytes"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+
+ "golang.org/x/crypto/bcrypt"
+)
+
+func processAction(w http.ResponseWriter, r *http.Request, action string) *ServerError {
+ switch action {
+ default:
+ return &ServerError{nil, "Invalid Action", 400}
+ case "cut":
+ return addSelectionToBuffer(w, r, cutBuffer)
+ case "copy":
+ return addSelectionToBuffer(w, r, copyBuffer)
+ case "cancel-cut":
+ return deleteBuffer(w, r, cutBuffer)
+ case "cancel-copy":
+ return deleteBuffer(w, r, copyBuffer)
+ case "cut-paste":
+ return moveFilesFromBuffer(w, r, cutBuffer)
+ case "copy-paste":
+ return pasteFilesFromBuffer(w, r, copyBuffer)
+ case "newdir":
+ return createNewDirectory(w, r)
+ case "delete":
+ return deleteSelectedFiles(w, r)
+ }
+}
+
+func viewHandler(w http.ResponseWriter, r *http.Request) *ServerError {
+ var err error
+ var serr *ServerError
+ for k, v := range r.URL.Query() {
+ switch k {
+ default:
+ http.Redirect(w, r, r.URL.Path, 302)
+ case "action":
+ return processAction(w, r, v[0])
+ }
+ }
+ fileNode, serr := getFileNode(r.URL.Path)
+ if serr != nil {
+ return serr
+ }
+ if fileNode.Info.Mode()&os.ModeSymlink != 0 {
+ fileURI := fileNode.URI
+ target := ""
+ target, fileNode, err = fileNode.EvalSymlinks()
+ if err != nil {
+ if !os.IsNotExist(err) {
+ return &ServerError{err, "", 500}
+ }
+ if len(target) != 0 {
+ return &ServerError{err, fileURI + ": broken link to '" + target + "'", 404}
+ } else {
+ return &ServerError{err, fileURI + ": Inaccessible link", 404}
+ }
+ }
+ }
+ if !fileNode.IsDir {
+ http.ServeFile(w, r, fileNode.Path)
+ return nil
+ }
+ dirList, err := getDirList(fileNode.Path, "name", true, true)
+ if err != nil {
+ return &ServerError{err, "", 404}
+ }
+
+ fileNode.Data = dirList
+ fmt.Printf("View: '%s'\n", fileNode.URI)
+ if strings.TrimSpace(fileNode.URI) == "" {
+ return renderTemplate(w, "viewHome", &FSData{
+ FileCount: len(dirList),
+ File: fileNode,
+ })
+ }
+
+ cutBuf, err := readBuffer(cutBuffer)
+ if err != nil {
+ return &ServerError{err, "", 500}
+ }
+ copyBuf, err := readBuffer(copyBuffer)
+ if err != nil {
+ return &ServerError{err, "", 500}
+ }
+ return renderTemplate(w, "viewDir", &FSData{
+ CutCount: len(cutBuf),
+ CutBuffer: cutBuf,
+ CopyCount: len(copyBuf),
+ CopyBuffer: copyBuf,
+ FileCount: len(dirList),
+ File: fileNode,
+ })
+}
+
+func profileHandler(w http.ResponseWriter, r *http.Request) *ServerError {
+ username, err := readData("username")
+ if err != nil {
+ return &ServerError{err, "", 500}
+ }
+
+ if r.Method == "GET" {
+ return renderTemplate(w, "profile", &Profile{
+ Username: string(username),
+ })
+ }
+
+ newUsername := strings.TrimSpace(r.FormValue("username"))
+ if newUsername == "" {
+ return renderTemplate(w, "profile", &Profile{
+ Message: "Username cannot be empty.",
+ Status: "warning",
+ })
+ }
+ if newUsername != string(username) {
+ if err := writeData("username", []byte(newUsername)); err != nil {
+ return &ServerError{err, "", 500}
+ }
+ }
+
+ newPassword := r.FormValue("password")
+ confirmPassword := r.FormValue("confirm-pass")
+ if newPassword != "" {
+ if newPassword != confirmPassword {
+ return renderTemplate(w, "profile", &Profile{
+ Username: string(username),
+ Message: "Passwords do not match.",
+ Status: "warning",
+ })
+ }
+ newPassHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
+ if err != nil {
+ return &ServerError{err, "", 500}
+ }
+ writeData("password", newPassHash)
+ }
+
+ return renderTemplate(w, "profile", &Profile{
+ Username: string(username),
+ Message: "Profile updated successfully.",
+ Status: "success",
+ })
+}
+
+func downloadHandler(w http.ResponseWriter, r *http.Request) *ServerError {
+ fmt.Printf("%s\n", r.Form)
+ fileNode, files, serr := getSelectedNodes(r)
+ if serr != nil {
+ return serr
+ }
+ if len(files) == 1 && !files[0].IsDir {
+ sendFile(w, r, files[0].Path)
+ return nil
+ }
+ zipName := fileNode.Info.Name() + ".zip"
+ target := "/tmp/cloud/" + zipName
+
+ archive, err := os.Create(target)
+ if err != nil {
+ return &ServerError{err, "", 500}
+ }
+ defer archive.Close()
+
+ zipWriter := zip.NewWriter(archive)
+ defer zipWriter.Close()
+
+ for _, file := range files {
+ err := addToZip(file.Path, zipWriter)
+ if err != nil {
+ return &ServerError{err, "", 500}
+ }
+ }
+ zipWriter.Close()
+ sendFile(w, r, target, zipName)
+ return nil
+}
+
+func uploadHandler(w http.ResponseWriter, r *http.Request) *ServerError {
+ fileNode, serr := getFileNode(r.URL.Path)
+ if serr != nil {
+ return serr
+ }
+ r.ParseMultipartForm(65536)
+ formData := r.MultipartForm
+
+ for _, handler := range formData.File["attachments"] {
+ fmt.Printf("%v\n", handler.Header)
+ fmt.Println(handler.Filename, ":", handler.Size)
+ file, err := handler.Open()
+ if err != nil {
+ return &ServerError{err, "", 500}
+ }
+ defer file.Close()
+ filepath := filepath.Join(fileNode.Path, handler.Filename)
+ fmt.Printf("Saving to %v...", filepath)
+ f, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE, 0666)
+ if err != nil {
+ return &ServerError{err, "", 500}
+ }
+ defer f.Close()
+ io.Copy(f, file)
+ fmt.Println("Saved.")
+ }
+ http.Redirect(w, r, "/view/"+fileNode.URI, 303)
+ return nil
+}
+
+func fileHandler(w http.ResponseWriter, r *http.Request) *ServerError {
+ fileNode, serr := getFileNode(r.URL.Path)
+ if serr != nil {
+ return serr
+ }
+ if fileNode.IsDir {
+ return &ServerError{nil, "File not Found.", 404}
+ }
+ http.ServeFile(w, r, fileNode.Path)
+ return nil
+}
+
+func handler(w http.ResponseWriter, r *http.Request) *ServerError {
+ if r.URL.Path != "/" {
+ return &ServerError{nil, "Invalid URL", 404}
+ }
+ http.Redirect(w, r, "view", 303)
+ return nil
+}
+
+type httpHandler func(http.ResponseWriter, *http.Request) *ServerError
+
+func (fn httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ username, password, ok := r.BasicAuth()
+ if !ok {
+ w.Header().Add("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
+ http.Error(w, "Basic Auth Missing.", 401)
+ return
+ }
+
+ realUsername, err := readData("username")
+ if err != nil {
+ http.Error(w, "Couldn't retreive username from server.", 500)
+ }
+ realPassword, err := readData("password")
+ if err != nil {
+ http.Error(w, "Couldn't retreive password from server.", 500)
+ }
+ realUsername = bytes.TrimRight(realUsername, "\n")
+ realPassword = bytes.TrimRight(realPassword, "\n")
+
+ // fmt.Printf("Given credentials: %s:%s\n", username, password)
+ // fmt.Printf("Requred credentials: %s:%s\n", realUsername, realPassword)
+
+ if string(realUsername) != username {
+ w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
+ http.Error(w, "Username not matched.", http.StatusUnauthorized)
+ return
+ }
+
+ if err := bcrypt.CompareHashAndPassword(realPassword, []byte(password)); err != nil {
+ w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
+ http.Error(w, "Authentication Error", http.StatusUnauthorized)
+ return
+ }
+
+ if serr := fn(w, r); serr != nil {
+ if serr.Err != nil {
+ fmt.Println("\n\nError Type:", reflect.TypeOf(serr.Err))
+ fmt.Println("Error Message:", serr.Error())
+ }
+ if serr.Message == "" {
+ serr.Message = "Internal Server Error"
+ }
+ http.Error(w, serr.Message, serr.Status)
+ }
+}
+
+func main() {
+ fileServer := http.FileServer(http.Dir("./static"))
+ http.Handle("/", httpHandler(handler))
+ http.Handle("/view/", httpHandler(viewHandler))
+ http.Handle("/profile/", httpHandler(profileHandler))
+ http.Handle("/upload/", httpHandler(uploadHandler))
+ http.Handle("/download/", httpHandler(downloadHandler))
+ http.Handle("/file/", httpHandler(fileHandler))
+ http.Handle("/static/", http.StripPrefix("/static/", fileServer))
+ fmt.Println("\nServer Listening on :8080")
+ log.Fatal(http.ListenAndServe(":8080", nil))
+}
diff --git a/src/static/icons/bs/actions/arrow-left-short.svg b/src/static/icons/bs/actions/arrow-left-short.svg
new file mode 100644
index 0000000..abb15dd
--- /dev/null
+++ b/src/static/icons/bs/actions/arrow-left-short.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left-short" viewBox="0 0 16 16">
+ <path fill-rule="evenodd" d="M12 8a.5.5 0 0 1-.5.5H5.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L5.707 7.5H11.5a.5.5 0 0 1 .5.5"/>
+</svg> \ No newline at end of file
diff --git a/src/static/icons/bs/actions/arrow-left.svg b/src/static/icons/bs/actions/arrow-left.svg
new file mode 100644
index 0000000..587d4fe
--- /dev/null
+++ b/src/static/icons/bs/actions/arrow-left.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left" viewBox="0 0 16 16">
+ <path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8"/>
+</svg> \ No newline at end of file
diff --git a/src/static/icons/bs/actions/arrow-right-short.svg b/src/static/icons/bs/actions/arrow-right-short.svg
new file mode 100644
index 0000000..fa238ff
--- /dev/null
+++ b/src/static/icons/bs/actions/arrow-right-short.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-right-short" viewBox="0 0 16 16">
+ <path fill-rule="evenodd" d="M4 8a.5.5 0 0 1 .5-.5h5.793L8.146 5.354a.5.5 0 1 1 .708-.708l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L10.293 8.5H4.5A.5.5 0 0 1 4 8"/>
+</svg> \ No newline at end of file
diff --git a/src/static/icons/bs/actions/arrow-right.svg b/src/static/icons/bs/actions/arrow-right.svg
new file mode 100644
index 0000000..2362904
--- /dev/null
+++ b/src/static/icons/bs/actions/arrow-right.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-right" viewBox="0 0 16 16">
+ <path fill-rule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8"/>
+</svg> \ No newline at end of file
diff --git a/src/static/icons/bs/actions/backspace.svg b/src/static/icons/bs/actions/backspace.svg
new file mode 100644
index 0000000..39b688f
--- /dev/null
+++ b/src/static/icons/bs/actions/backspace.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-backspace" viewBox="0 0 16 16">
+ <path d="M5.83 5.146a.5.5 0 0 0 0 .708L7.975 8l-2.147 2.146a.5.5 0 0 0 .707.708l2.147-2.147 2.146 2.147a.5.5 0 0 0 .707-.708L9.39 8l2.146-2.146a.5.5 0 0 0-.707-.708L8.683 7.293 6.536 5.146a.5.5 0 0 0-.707 0z"/>
+ <path d="M13.683 1a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-7.08a2 2 0 0 1-1.519-.698L.241 8.65a1 1 0 0 1 0-1.302L5.084 1.7A2 2 0 0 1 6.603 1zm-7.08 1a1 1 0 0 0-.76.35L1 8l4.844 5.65a1 1 0 0 0 .759.35h7.08a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1z"/>
+</svg> \ No newline at end of file
diff --git a/src/static/icons/bs/actions/check.svg b/src/static/icons/bs/actions/check.svg
new file mode 100644
index 0000000..11ab547
--- /dev/null
+++ b/src/static/icons/bs/actions/check.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
+ <path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z"/>
+</svg> \ No newline at end of file
diff --git a/src/static/icons/bs/actions/clipboard.svg b/src/static/icons/bs/actions/clipboard.svg
new file mode 100644
index 0000000..b92f42a
--- /dev/null
+++ b/src/static/icons/bs/actions/clipboard.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
+ <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1z"/>
+ <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0z"/>
+</svg> \ No newline at end of file
diff --git a/src/static/icons/bs/actions/copy.svg b/src/static/icons/bs/actions/copy.svg
new file mode 100644
index 0000000..b590680
--- /dev/null
+++ b/src/static/icons/bs/actions/copy.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-copy" viewBox="0 0 16 16">
+ <path fill-rule="evenodd" d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zM2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1z"/>
+</svg> \ No newline at end of file
diff --git a/src/static/icons/bs/actions/download.svg b/src/static/icons/bs/actions/download.svg
new file mode 100644
index 0000000..90a34a3
--- /dev/null
+++ b/src/static/icons/bs/actions/download.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-download" viewBox="0 0 16 16">
+ <path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/>
+ <path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z"/>
+</svg> \ No newline at end of file
diff --git a/src/static/icons/bs/actions/folder-plus.svg b/src/static/icons/bs/actions/folder-plus.svg
new file mode 100644
index 0000000..85b5a18
--- /dev/null
+++ b/src/static/icons/bs/actions/folder-plus.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-folder-plus" viewBox="0 0 16 16">
+ <path d="m.5 3 .04.87a2 2 0 0 0-.342 1.311l.637 7A2 2 0 0 0 2.826 14H9v-1H2.826a1 1 0 0 1-.995-.91l-.637-7A1 1 0 0 1 2.19 4h11.62a1 1 0 0 1 .996 1.09L14.54 8h1.005l.256-2.819A2 2 0 0 0 13.81 3H9.828a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 6.172 1H2.5a2 2 0 0 0-2 2m5.672-1a1 1 0 0 1 .707.293L7.586 3H2.19q-.362.002-.683.12L1.5 2.98a1 1 0 0 1 1-.98z"/>
+ <path d="M13.5 9a.5.5 0 0 1 .5.5V11h1.5a.5.5 0 1 1 0 1H14v1.5a.5.5 0 1 1-1 0V12h-1.5a.5.5 0 0 1 0-1H13V9.5a.5.5 0 0 1 .5-.5"/>
+</svg> \ No newline at end of file
diff --git a/src/static/icons/bs/actions/folder.svg b/src/static/icons/bs/actions/folder.svg
new file mode 100644
index 0000000..a30c452
--- /dev/null
+++ b/src/static/icons/bs/actions/folder.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-folder" viewBox="0 0 16 16">
+ <path d="M.54 3.87.5 3a2 2 0 0 1 2-2h3.672a2 2 0 0 1 1.414.586l.828.828A2 2 0 0 0 9.828 3h3.982a2 2 0 0 1 1.992 2.181l-.637 7A2 2 0 0 1 13.174 14H2.826a2 2 0 0 1-1.991-1.819l-.637-7a2 2 0 0 1 .342-1.31zM2.19 4a1 1 0 0 0-.996 1.09l.637 7a1 1 0 0 0 .995.91h10.348a1 1 0 0 0 .995-.91l.637-7A1 1 0 0 0 13.81 4zm4.69-1.707A1 1 0 0 0 6.172 2H2.5a1 1 0 0 0-1 .981l.006.139q.323-.119.684-.12h5.396z"/>
+</svg> \ No newline at end of file
diff --git a/src/static/icons/bs/actions/scissors.svg b/src/static/icons/bs/actions/scissors.svg
new file mode 100644
index 0000000..2f566e4
--- /dev/null
+++ b/src/static/icons/bs/actions/scissors.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-scissors" viewBox="0 0 16 16">
+ <path d="M3.5 3.5c-.614-.884-.074-1.962.858-2.5L8 7.226 11.642 1c.932.538 1.472 1.616.858 2.5L8.81 8.61l1.556 2.661a2.5 2.5 0 1 1-.794.637L8 9.73l-1.572 2.177a2.5 2.5 0 1 1-.794-.637L7.19 8.61zm2.5 10a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0m7 0a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0"/>
+</svg> \ No newline at end of file
diff --git a/src/static/icons/bs/actions/trash.svg b/src/static/icons/bs/actions/trash.svg
new file mode 100644
index 0000000..3020264
--- /dev/null
+++ b/src/static/icons/bs/actions/trash.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
+ <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z"/>
+ <path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z"/>
+</svg> \ No newline at end of file
diff --git a/src/static/icons/bs/actions/upload.svg b/src/static/icons/bs/actions/upload.svg
new file mode 100644
index 0000000..9a4a363
--- /dev/null
+++ b/src/static/icons/bs/actions/upload.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-upload" viewBox="0 0 16 16">
+ <path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/>
+ <path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708z"/>
+</svg> \ No newline at end of file
diff --git a/src/static/icons/bs/files/file-earmark.svg b/src/static/icons/bs/files/file-earmark.svg
new file mode 100644
index 0000000..eb55426
--- /dev/null
+++ b/src/static/icons/bs/files/file-earmark.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#abb" class="bi bi-file-earmark" viewBox="0 0 16 16">
+ <path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5z"/>
+</svg>
diff --git a/src/static/icons/bs/files/folder-symlink.svg b/src/static/icons/bs/files/folder-symlink.svg
new file mode 100644
index 0000000..72fbd2e
--- /dev/null
+++ b/src/static/icons/bs/files/folder-symlink.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#7cc" class="bi bi-folder-symlink" viewBox="0 0 16 16">
+ <path d="m11.798 8.271-3.182 1.97c-.27.166-.616-.036-.616-.372V9.1s-2.571-.3-4 2.4c.571-4.8 3.143-4.8 4-4.8v-.769c0-.336.346-.538.616-.371l3.182 1.969c.27.166.27.576 0 .742"/>
+ <path d="m.5 3 .04.87a2 2 0 0 0-.342 1.311l.637 7A2 2 0 0 0 2.826 14h10.348a2 2 0 0 0 1.991-1.819l.637-7A2 2 0 0 0 13.81 3H9.828a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 6.172 1H2.5a2 2 0 0 0-2 2m.694 2.09A1 1 0 0 1 2.19 4h11.62a1 1 0 0 1 .996 1.09l-.636 7a1 1 0 0 1-.996.91H2.826a1 1 0 0 1-.995-.91zM6.172 2a1 1 0 0 1 .707.293L7.586 3H2.19q-.362.002-.683.12L1.5 2.98a1 1 0 0 1 1-.98z"/>
+</svg>
diff --git a/src/static/icons/bs/files/folder2.svg b/src/static/icons/bs/files/folder2.svg
new file mode 100644
index 0000000..be82283
--- /dev/null
+++ b/src/static/icons/bs/files/folder2.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#8bf" class="bi bi-folder2" viewBox="0 0 16 16">
+ <path d="M1 3.5A1.5 1.5 0 0 1 2.5 2h2.764c.958 0 1.76.56 2.311 1.184C7.985 3.648 8.48 4 9 4h4.5A1.5 1.5 0 0 1 15 5.5v7a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 12.5zM2.5 3a.5.5 0 0 0-.5.5V6h12v-.5a.5.5 0 0 0-.5-.5H9c-.964 0-1.71-.629-2.174-1.154C6.374 3.334 5.82 3 5.264 3zM14 7H2v5.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5z"/>
+</svg>
diff --git a/src/static/icons/bs/files/link-45deg.svg b/src/static/icons/bs/files/link-45deg.svg
new file mode 100644
index 0000000..fb527fa
--- /dev/null
+++ b/src/static/icons/bs/files/link-45deg.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#3dd" class="bi bi-link-45deg" viewBox="0 0 16 16">
+ <path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1 1 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4 4 0 0 1-.128-1.287z"/>
+ <path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/>
+</svg>
diff --git a/src/static/icons/bs/files/link-broken-45deg.svg b/src/static/icons/bs/files/link-broken-45deg.svg
new file mode 100644
index 0000000..4f20cd3
--- /dev/null
+++ b/src/static/icons/bs/files/link-broken-45deg.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#d99" class="bi bi-link-45deg" viewBox="0 0 16 16">
+ <path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1 1 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4 4 0 0 1-.128-1.287z"/>
+ <path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/>
+</svg>
diff --git a/src/static/icons/bs/files/question.svg b/src/static/icons/bs/files/question.svg
new file mode 100644
index 0000000..ba185ad
--- /dev/null
+++ b/src/static/icons/bs/files/question.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-question" viewBox="0 0 16 16">
+ <path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286m1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94"/>
+</svg> \ No newline at end of file
diff --git a/src/static/icons/bs/hdd.svg b/src/static/icons/bs/hdd.svg
new file mode 100644
index 0000000..46cc4e4
--- /dev/null
+++ b/src/static/icons/bs/hdd.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#ddd" class="bi bi-hdd" viewBox="0 0 16 16">
+ <path d="M4.5 11a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1M3 10.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0"/>
+ <path d="M16 11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V9.51c0-.418.105-.83.305-1.197l2.472-4.531A1.5 1.5 0 0 1 4.094 3h7.812a1.5 1.5 0 0 1 1.317.782l2.472 4.53c.2.368.305.78.305 1.198zM3.655 4.26 1.592 8.043Q1.79 8 2 8h12q.21 0 .408.042L12.345 4.26a.5.5 0 0 0-.439-.26H4.094a.5.5 0 0 0-.44.26zM1 10v1a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-1a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1"/>
+</svg>
diff --git a/src/static/icons/bs/house-door.svg b/src/static/icons/bs/house-door.svg
new file mode 100644
index 0000000..1b0a602
--- /dev/null
+++ b/src/static/icons/bs/house-door.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#add" class="bi bi-house-door" viewBox="0 0 16 16">
+ <path d="M8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4.5a.5.5 0 0 0 .5-.5v-4h2v4a.5.5 0 0 0 .5.5H14a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293zM2.5 14V7.707l5.5-5.5 5.5 5.5V14H10v-4a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5v4z"/>
+</svg>
diff --git a/src/static/icons/bs/person-circle.svg b/src/static/icons/bs/person-circle.svg
new file mode 100644
index 0000000..b2c0c28
--- /dev/null
+++ b/src/static/icons/bs/person-circle.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#add" class="bi bi-person-circle" viewBox="0 0 16 16">
+ <path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0"/>
+ <path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1"/>
+</svg>
diff --git a/src/static/script.js b/src/static/script.js
new file mode 100644
index 0000000..8c1bf51
--- /dev/null
+++ b/src/static/script.js
@@ -0,0 +1,41 @@
+"use strict";
+"use warnings";
+
+function checkAll(checked, scope) {
+ let inputs = scope.getElementsByTagName('input');
+ for (var i = 0; i < inputs.length; i++) {
+ if (inputs[i].type.toLowerCase() == 'checkbox') {
+ inputs[i].checked = checked;
+ }
+ }
+}
+
+function toggleAll(checkbox) {
+ let scope = document.getElementById('file-list')
+ if (checkbox.checked) {
+ checkAll(true, scope);
+ } else {
+ checkAll(false, scope);
+ }
+}
+
+function deleteAction(button) {
+ let table = document.getElementById('file-list')
+ let tbody = table.getElementsByClassName('tbody')[0]
+ let inputs = tbody.getElementsByTagName('input')
+ let msg = 0;
+ for (var i = 0; i < inputs.length; i++) {
+ inputs[i].type.toLowerCase() == 'checkbox' &&
+ inputs[i].checked && msg++;
+ }
+ if (msg == 0) {
+ msg = "Whole Folder will be deleted."
+ } else {
+ msg += " file(s) selected."
+ }
+ if (confirm(msg + " Confirm Deletion?")) {
+ button.formAction = "?action=delete";
+ button.onclick = "submit()";
+ button.click();
+ }
+}
diff --git a/src/static/style.css b/src/static/style.css
new file mode 100644
index 0000000..b24c197
--- /dev/null
+++ b/src/static/style.css
@@ -0,0 +1,356 @@
+:root {
+ --profile-width: 15rem;
+}
+
+body {
+ background-color: #1b1b1b;
+ color: lightgrey;
+ font-family: "Fira Sans";
+ margin: auto;
+ margin-bottom: 10rem;
+ max-width: 60rem;
+}
+
+header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem;
+}
+
+header h1 {
+ color: orange;
+}
+
+header img {
+ width: 2rem;
+ padding: 1rem;
+}
+
+a {
+ color: #7bf;
+ text-decoration: none;
+}
+
+button b {
+ vertical-align: middle;
+ font-size: 0.8rem;
+}
+
+label b {
+ font-size: 0.9rem;
+}
+
+button img, details img {
+ padding: 0.2rem;
+ width: 1rem;
+ vertical-align: middle;
+}
+
+#newdir-cb+label {
+ background-color: #2d60ba;
+ opacity: 0.6;
+ position: fixed;
+ bottom: 10%;
+ right: 10%;
+ border: 0;
+ border-radius: 60%;
+ padding: 0.6rem 0.85rem;
+ z-index: 11;
+}
+
+#newdir-cb +label:hover {
+ opacity: 1;
+ bottom: 10%;
+ right: 10%;
+ padding: 0.8rem 1.05rem;
+}
+
+#newdir-cb:checked +label {
+ opacity: 0.8;
+}
+
+#newdir-cb+label img {
+ width: 1.5rem;
+ margin-top: 0.2rem;
+}
+
+#newdir-bg {
+ cursor: pointer;
+ background-color: black;
+ opacity: 50%;
+ display: none;
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ z-index: 10;
+}
+
+#newdir-box {
+ color: white;
+ background: steelblue;
+ display: none;
+ position: fixed;
+ right: 10%;
+ bottom: 20%;
+ text-align: center;
+ width: 15rem;
+ padding: 1rem;
+ z-index: 11;
+}
+
+#newdir-box input[type="text"] {
+ width: 10rem;
+}
+
+#newdir-box button {
+ width: 2rem;
+ height: 1.6rem;
+}
+
+#newdir-box img {
+ height: 1.4rem;
+ width: 1.6rem;
+ margin-top: -0.2rem;
+ margin-left: -0.4rem;
+}
+
+#newdir-box img:hover {
+ background-color: lightgreen;
+}
+
+#newdir-cb { display: none; }
+#newdir-cb:checked ~ #newdir-box { display: block; }
+#newdir-cb:checked ~ #newdir-bg { display: block; }
+
+.form-header label img {
+ padding: 0.2rem;
+ width: 1rem;
+ vertical-align: middle;
+}
+
+.html-path {
+ padding: 1rem 0rem;
+ overflow-x: auto;
+ white-space: nowrap;
+}
+
+.html-path a {
+ background-color: #333;
+ border-radius: 0.5rem;
+ color: #add;
+ font: 1.5rem;
+ padding: 0.3rem 0.7rem;
+ margin: 0.5rem;
+}
+
+.form-actions {
+ white-space: nowrap;
+}
+
+.clickable::-moz-selection,
+.clickable::selection,
+.clickable *::-moz-selection,
+.clickable *::selection {
+ background-color: transparent;
+}
+
+.back-button img {
+ width: 1.8rem;
+ margin: -0.4rem;
+}
+
+#upload-button + label,
+.form-actions button,
+.form-actions a {
+ background-color: #bdd;
+ color: black;
+ padding: 0.3rem 0.5rem;
+ margin-right: 0.5rem;
+ vertical-align: middle;
+ border: none;
+}
+
+#upload-button + label:hover,
+.form-actions button:hover {
+ background-color: #8cc;
+}
+
+#upload-button { display: none; }
+#upload-button + label { display: inline-block; }
+#upload-button:checked + label { background-color: #7dd; }
+#upload-button:checked ~ #file-upload-box { display: block; }
+
+#file-upload-box {
+ background: steelblue;
+ display: none;
+ padding: 0.7rem 1rem;
+ margin: 1rem 0rem;
+}
+
+#file-upload-box button {
+ margin-top: -0.25rem;
+}
+
+#file-upload-box { display: none; }
+
+details { margin: 1rem 0rem; background-color: #aa3333; }
+details summary { padding: 0.5rem 1rem; background-color: #cc3333 }
+details[open] summary { color: #111; }
+details summary span { vertical-align: middle; margin: -0.2rem; }
+details .content { padding: 0.5rem 1rem; }
+ul { list-style-type: none; }
+
+.actions { float: right; }
+
+.table { display: table; }
+.thead { display: table-header-group; }
+.tbody { display: table-row-group; }
+.tr { display: table-row; }
+.th, .td { display: table-cell; }
+
+#file-list {
+ color: #dddddd;
+ width: 100%;
+ margin: 1rem 0rem;
+ overflow-x: auto;
+}
+#file-list .thead { background-color: #2b2b2b; }
+#file-list .thead:has(input[type=checkbox]:checked) { background-color: #557; }
+#file-list .th { font-weight: bold; }
+#file-list .tr:hover { background-color: #2b2b2b; }
+#file-list .tr:has(input[type=checkbox]:checked) { background-color: #1b2b3b; }
+#file-list .tr:has(input[type=checkbox]:checked):hover { background-color: #2b3b4b; }
+#file-list .td, #file-list .tr, #file-list .th {
+ border-bottom: 0.1rem solid #333;
+ overflow: hidden;
+ padding: 0.5rem;
+ font-size: 0.97em;
+}
+#file-list img { width: 1rem; }
+#file-list a {
+ color: #9cf;
+ display: block;
+ margin: -0.8rem;
+ padding: 1rem;
+ font-size: 1rem;
+}
+#file-list a:hover {
+ text-decoration-line: underline;
+}
+
+.info {
+ color: lightgray;
+ font-size: 0.92em;
+ text-align: center;
+ padding: 1rem 2rem;
+}
+
+.drives {
+ padding: 1rem;
+}
+
+.drives a {
+ background-color: #2b2b2b;
+ display: flex;
+ align-items: center;
+ padding: 0.5em;
+ margin: 1rem 0rem;
+}
+
+.drives a:hover {
+ background-color: #1b2b3b;
+}
+
+.drives a span { padding: 0.5rem; }
+.drives a img { width: 2rem; }
+.drives .desc { color: gray; }
+
+#profile {
+ background-color: #2b2b3b;
+ display: table;
+ align-items: center;
+ box-sizing: border-box;
+ padding: 3rem 5rem;
+ max-width: var(--profile-width);
+ margin: auto;
+ margin-top: 2rem;
+}
+
+#profile h2 {
+ text-align: center;
+}
+
+#profile label,
+#profile input {
+ height: 1.5rem;
+ margin: 0.3rem 0rem;
+ display: block;
+ width: 100%;
+}
+
+#profile label {
+ font-weight: bold;
+ margin: 2rem 0rem 0rem 0rem;
+}
+
+#profile input {
+ width: var(--profile-width);
+}
+
+#profile button {
+ color: black;
+ background-color: #f66;
+ font-weight: bold;
+ vertical-align: center;
+ height: 2rem;
+ width: 100%;
+ margin-top: 3rem;
+ border: none;
+}
+
+#profile button:hover {
+ background-color: #f88;
+ cursor: pointer;
+}
+
+#profile p {
+ max-width: var(--profile-width);
+}
+
+.warning { color: red; }
+.success { color: lightgreen; }
+
+
+@media screen and (min-width: 20rem) {
+ #upload-button+label { float: right; }
+}
+
+@media screen and (max-width: 60rem) {
+ #file-list .tr .th:nth-child(6),
+ #file-list .tr .td:nth-child(6) { display: none; }
+}
+
+@media screen and (max-width: 40rem) {
+ :root {
+ --profile-width: 10rem;
+ }
+ #profile { padding: 2rem 3rem; }
+
+ button b { display: none; }
+ button { margin-right: 0 !important; }
+ #file-list .tr .th:nth-child(7),
+ #file-list .tr .td:nth-child(7) { display: none; }
+ .hint { display: none; }
+}
+
+@media screen and (max-width: 25rem) {
+ label b { display: none; }
+ #file-list .tr .th:nth-child(5),
+ #file-list .tr .td:nth-child(5) { display: none; }
+}
+
diff --git a/src/templates.go b/src/templates.go
new file mode 100644
index 0000000..ed88874
--- /dev/null
+++ b/src/templates.go
@@ -0,0 +1,45 @@
+package main
+
+import (
+ "net/http"
+ "html/template"
+)
+
+type FSData struct {
+ CutCount int
+ CopyCount int
+ FileCount int
+ CutBuffer []string
+ CopyBuffer []string
+ File *FileNode
+}
+
+type Profile struct {
+ Username string
+ Password string
+ Message string
+ Status string
+}
+
+var templates = make(map[string]*template.Template)
+
+func renderTemplate(w http.ResponseWriter, tmpl string, data any) *ServerError {
+ if err := templates[tmpl].ExecuteTemplate(w, "base.html", data); err != nil {
+ return &ServerError{err, "", 500}
+ }
+ return nil
+}
+
+func init() {
+ templates["profile"] = template.Must(template.New(
+ "viewDir.html",
+ ).ParseFiles("templates/base.html", "templates/profile.html"))
+
+ templates["viewDir"] = template.Must(template.New(
+ "viewDir.html",
+ ).ParseFiles("templates/base.html", "templates/viewDir.html"))
+
+ templates["viewHome"] = template.Must(template.New(
+ "viewHome.html",
+ ).ParseFiles("templates/base.html", "templates/viewHome.html"))
+}
diff --git a/src/templates/base.html b/src/templates/base.html
new file mode 100644
index 0000000..98e5787
--- /dev/null
+++ b/src/templates/base.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Cloud Maker</title>
+ <link rel="stylesheet" href="/static/style.css">
+ <script src="/static/script.js"></script>
+</head>
+
+<body>
+ <main>
+ {{ template "main" . }}
+ </main>
+</body>
+
+</html>
+
diff --git a/src/templates/profile.html b/src/templates/profile.html
new file mode 100644
index 0000000..6e6fccd
--- /dev/null
+++ b/src/templates/profile.html
@@ -0,0 +1,24 @@
+{{ define "main" }}
+ <header>
+ <a href="/"><h1>Cloud Maker</h1></a>
+ <a href="/"><img src="/static/icons/bs/house-door.svg"></a>
+ </header>
+
+ <form id="profile" name="profile" method="POST">
+ <h2>Profile</h2>
+
+ <label for="username">Username</label>
+ <input id="username" name="username" value="{{.Username}}" required>
+
+ <label for="password">New Password</label>
+ <input type="password" id="password" name="password">
+
+ <label for="confirm-pass">Confirm Password</label>
+ <input type="password" id="confirm-pass" name="confirm-pass">
+
+ <button>Update</button>
+ {{ if .Message }}
+ <p class="{{.Status}}">{{ .Message }}</p>
+ {{ end }}
+ </form>
+{{ end }}
diff --git a/src/templates/viewDir.html b/src/templates/viewDir.html
new file mode 100644
index 0000000..9d45242
--- /dev/null
+++ b/src/templates/viewDir.html
@@ -0,0 +1,106 @@
+{{ define "main" }}
+{{ $location := .File.URI }}
+{{ $path := .File.Path }}
+<form id="file_manager" name="file_manager" enctype="multipart/form-data" method="post">
+
+ <div class="form-header">
+ <h4 class="html-path clickable">{{ .File.HTMLPath }}</h4>
+ <div class="form-actions clickable">
+ <button class="back-button" formaction="/view/{{$location}}/.."> <img src="/static/icons/bs/actions/arrow-left-short.svg"> <b>Back</b> </button>
+ <button class="cut-button" formaction="?action=cut"> <img src="/static/icons/bs/actions/scissors.svg"> <b>Cut</b> </button>
+ <button class="copy-button" formaction="?action=copy"> <img src="/static/icons/bs/actions/copy.svg"> <b>Copy</b> </button>
+ <button class="delete-button" onclick="deleteAction(this)"> <img src="/static/icons/bs/actions/trash.svg"> <b>Delete</b> </button>
+ <button class="download-button" formaction="/download/{{$location}}"> <img src="/static/icons/bs/actions/download.svg"> <b>Download</b> </button>
+ <input type="checkbox" id="upload-button">
+ <label for="upload-button"><img src="/static/icons/bs/actions/upload.svg"> <b>Upload</b> </label>
+ <div id="file-upload-box">
+ <input type="file" name="attachments" multiple>
+ <span class="actions">
+ <button formaction="/upload/{{$location}}"><img src="/static/icons/bs/actions/upload.svg"> <b>Submit</b> </button>
+ </span>
+ </div>
+ </div>
+
+ {{ if .CutCount }}
+ <details id="cut-buffer">
+ <summary class="clickable">
+ <span> <img src="/static/icons/bs/actions/scissors.svg"> Files in Cut Buffer ({{ .CutCount }}) </span>
+ <span class="actions">
+ <button type="submit" class="paste-button" formaction="?action=cut-paste">
+ <img src="/static/icons/bs/actions/clipboard.svg"> <b>Paste</b>
+ </button>
+ <button type="submit" class="clear-button" formaction="?action=cancel-cut">
+ <img src="/static/icons/bs/actions/backspace.svg"> <b>Clear</b>
+ </button>
+ </span>
+ </summary>
+ <div class="content">
+ {{ range .CutBuffer }} <div>{{ . }}</div> {{ end }}
+ </div>
+ </details>
+ {{ end }}
+
+ {{ if .CopyCount }}
+ <details id="copy-buffer">
+ <summary class="clickable">
+ <span> <img class="icon" src="/static/icons/bs/actions/copy.svg"> Files in Copy Buffer ({{ .CopyCount }}) </span>
+ <span class="actions">
+ <button type="submit" class="paste-button" formaction="/view/{{$location}}?action=copy-paste"> <img src="/static/icons/bs/actions/clipboard.svg"> <b>Paste</b> </button>
+ <button type="submit" class="clear-button" formaction="/view/{{$location}}?action=cancel-copy"> <img src="/static/icons/bs/actions/backspace.svg"> <b>Clear</b> </button>
+ </span>
+ </summary>
+ <div class="content">
+ {{ range .CopyBuffer }} <div>{{ . }}</div> {{ end }}
+ </div>
+ </details>
+ {{ end }}
+ </div>
+
+ <div class="table clickable" id="file-list">
+ <div class="thead">
+ <label class="tr" for="file-list-header">
+ <input class="th" type="checkbox" id="file-list-header" name="file-list-header" onchange="toggleAll(this)">
+ <span class="th"></span>
+ <span class="th">File</span>
+ <span class="th">Size</span>
+ <span class="th">Date</span>
+ <span class="th">Time</span>
+ <span class="th">Info</span>
+ </label>
+ </div>
+ <div class="tbody">
+ {{ range .File.Data }}
+ {{ $name := .Info.Name }}
+ <label class="tr mode-{{.Mode}}" for="-file-entry--{{$name}}">
+ <input class="td" type="checkbox" id="-file-entry--{{$name}}" name="-file-entry--{{$name}}">
+ <span class="td"><img src="{{.IconPath}}"></span>
+ <span class="td"><a href="/view/{{$location}}/{{$name}}">{{$name}}</a></span>
+ <span class="td">{{ .Size }}</span>
+ <span class="td">{{ .ModDate }}</span>
+ <span class="td">{{ .ModTime }}</span>
+ <span class="td">{{ .Details }}</span>
+ </label>
+ {{ end }}
+ </div>
+ </div>
+
+ {{ if .FileCount }}
+ <p class="info hint">Hint: aestrik (*) indicates mime-type (media type) of file in Info section.</p>
+ {{ else }}
+ <p class="info empty-folder-msg">Empty Folder</p>
+ {{ end }}
+</form>
+
+<form action="?action=newdir" method="post">
+ <input type="checkbox" id="newdir-cb" name="newdir-cb">
+ <label for="newdir-cb"><img src="/static/icons/bs/actions/folder-plus.svg"></label>
+ <div id="newdir-bg" onclick="document.getElementById('newdir-cb').checked = false;"></div>
+ <div id="newdir-box">
+ <p><b>Create New Folder</b></p>
+ <p>
+ <input type="text" name="newdir" placeholder="Folder Name" required>
+ <button formaction="?action=newdir"><img src="/static/icons/bs/actions/check.svg"></button>
+ </p>
+ </div>
+</form>
+{{ end }}
diff --git a/src/templates/viewHome.html b/src/templates/viewHome.html
new file mode 100644
index 0000000..bd841d2
--- /dev/null
+++ b/src/templates/viewHome.html
@@ -0,0 +1,29 @@
+{{ define "main" }}
+{{ $location := .File.URI }}
+{{ $path := .File.Path }}
+<header>
+ <a href="/"><h1>Cloud Maker</h1></a>
+ <a href="/profile"><img src="/static/icons/bs/person-circle.svg"></a>
+</header>
+
+<form id="device_manager" name="device_manager" method="POST">
+
+ <div class="drives">
+ {{ range .File.Data }}
+ {{ $name := .Info.Name }}
+ <a class="drive" href="/view/{{$location}}/{{$name}}">
+ <span><img src="/static/icons/bs/hdd.svg"></span>
+ <span>
+ <div>{{ $name }}</div>
+ <div class="desc">drive</div>
+ </span>
+ </a>
+ {{ end }}
+ </div>
+
+ {{ if not .FileCount }}
+ <p class="info">No Drives Mounted.</p>
+ {{ end }}
+
+</form>
+{{ end }}