From 6a16bbdcdb40406592e47ee8d489f857837e5c96 Mon Sep 17 00:00:00 2001 From: Vikas Kushwaha Date: Thu, 21 Nov 2024 13:54:38 +0530 Subject: Initial commit --- src/copy.go | 124 ++++++ src/files.go | 449 ++++++++++++++++++++++ src/go.mod | 5 + src/go.sum | 2 + src/helpers.go | 292 ++++++++++++++ src/server.go | 298 ++++++++++++++ src/static/icons/bs/actions/arrow-left-short.svg | 3 + src/static/icons/bs/actions/arrow-left.svg | 3 + src/static/icons/bs/actions/arrow-right-short.svg | 3 + src/static/icons/bs/actions/arrow-right.svg | 3 + src/static/icons/bs/actions/backspace.svg | 4 + src/static/icons/bs/actions/check.svg | 3 + src/static/icons/bs/actions/clipboard.svg | 4 + src/static/icons/bs/actions/copy.svg | 3 + src/static/icons/bs/actions/download.svg | 4 + src/static/icons/bs/actions/folder-plus.svg | 4 + src/static/icons/bs/actions/folder.svg | 3 + src/static/icons/bs/actions/scissors.svg | 3 + src/static/icons/bs/actions/trash.svg | 4 + src/static/icons/bs/actions/upload.svg | 4 + src/static/icons/bs/files/file-earmark.svg | 3 + src/static/icons/bs/files/folder-symlink.svg | 4 + src/static/icons/bs/files/folder2.svg | 3 + src/static/icons/bs/files/link-45deg.svg | 4 + src/static/icons/bs/files/link-broken-45deg.svg | 4 + src/static/icons/bs/files/question.svg | 3 + src/static/icons/bs/hdd.svg | 4 + src/static/icons/bs/house-door.svg | 3 + src/static/icons/bs/person-circle.svg | 4 + src/static/script.js | 41 ++ src/static/style.css | 356 +++++++++++++++++ src/templates.go | 45 +++ src/templates/base.html | 17 + src/templates/profile.html | 24 ++ src/templates/viewDir.html | 106 +++++ src/templates/viewHome.html | 29 ++ 36 files changed, 1868 insertions(+) create mode 100644 src/copy.go create mode 100644 src/files.go create mode 100644 src/go.mod create mode 100644 src/go.sum create mode 100644 src/helpers.go create mode 100644 src/server.go create mode 100644 src/static/icons/bs/actions/arrow-left-short.svg create mode 100644 src/static/icons/bs/actions/arrow-left.svg create mode 100644 src/static/icons/bs/actions/arrow-right-short.svg create mode 100644 src/static/icons/bs/actions/arrow-right.svg create mode 100644 src/static/icons/bs/actions/backspace.svg create mode 100644 src/static/icons/bs/actions/check.svg create mode 100644 src/static/icons/bs/actions/clipboard.svg create mode 100644 src/static/icons/bs/actions/copy.svg create mode 100644 src/static/icons/bs/actions/download.svg create mode 100644 src/static/icons/bs/actions/folder-plus.svg create mode 100644 src/static/icons/bs/actions/folder.svg create mode 100644 src/static/icons/bs/actions/scissors.svg create mode 100644 src/static/icons/bs/actions/trash.svg create mode 100644 src/static/icons/bs/actions/upload.svg create mode 100644 src/static/icons/bs/files/file-earmark.svg create mode 100644 src/static/icons/bs/files/folder-symlink.svg create mode 100644 src/static/icons/bs/files/folder2.svg create mode 100644 src/static/icons/bs/files/link-45deg.svg create mode 100644 src/static/icons/bs/files/link-broken-45deg.svg create mode 100644 src/static/icons/bs/files/question.svg create mode 100644 src/static/icons/bs/hdd.svg create mode 100644 src/static/icons/bs/house-door.svg create mode 100644 src/static/icons/bs/person-circle.svg create mode 100644 src/static/script.js create mode 100644 src/static/style.css create mode 100644 src/templates.go create mode 100644 src/templates/base.html create mode 100644 src/templates/profile.html create mode 100644 src/templates/viewDir.html create mode 100644 src/templates/viewHome.html (limited to 'src') 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 += `` + "Home" + ` ` + p := strings.Split(fileNode.URI, string(os.PathSeparator)) + for i, dir := range p { + if p[i] != "" { + htmlpath += `> ` + dir + ` ` + } + } + 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 @@ + + + \ 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 @@ + + + \ 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 @@ + + + \ 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 @@ + + + \ 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 @@ + + + + \ 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 @@ + + + \ 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 @@ + + + + \ 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 @@ + + + \ 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 @@ + + + + \ 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 @@ + + + + \ 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 @@ + + + \ 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 @@ + + + \ 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 @@ + + + + \ 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 @@ + + + + \ 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 @@ + + + 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 @@ + + + + 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 @@ + + + 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 @@ + + + + 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 @@ + + + + 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 @@ + + + \ 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 @@ + + + + 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 @@ + + + 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 @@ + + + + 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 @@ + + + + + Cloud Maker + + + + + +
+ {{ template "main" . }} +
+ + + + 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" }} +
+

Cloud Maker

+ +
+ +
+

Profile

+ + + + + + + + + + + + {{ if .Message }} +

{{ .Message }}

+ {{ end }} +
+{{ 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 }} +
+ +
+

{{ .File.HTMLPath }}

+
+ + + + + + + +
+ + + + +
+
+ + {{ if .CutCount }} +
+ + Files in Cut Buffer ({{ .CutCount }}) + + + + + +
+ {{ range .CutBuffer }}
{{ . }}
{{ end }} +
+
+ {{ end }} + + {{ if .CopyCount }} +
+ + Files in Copy Buffer ({{ .CopyCount }}) + + + + + +
+ {{ range .CopyBuffer }}
{{ . }}
{{ end }} +
+
+ {{ end }} +
+ +
+
+ +
+
+ {{ range .File.Data }} + {{ $name := .Info.Name }} + + {{ end }} +
+
+ + {{ if .FileCount }} +

Hint: aestrik (*) indicates mime-type (media type) of file in Info section.

+ {{ else }} +

Empty Folder

+ {{ end }} +
+ +
+ + +
+
+

Create New Folder

+

+ + +

+
+
+{{ 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 }} +
+

Cloud Maker

+ +
+ +
+ +
+ {{ range .File.Data }} + {{ $name := .Info.Name }} + + + +
{{ $name }}
+
drive
+
+
+ {{ end }} +
+ + {{ if not .FileCount }} +

No Drives Mounted.

+ {{ end }} + +
+{{ end }} -- cgit v1.2.3