diff options
author | Vikas Kushwaha <dev@vikas.rocks> | 2024-11-21 13:54:38 +0530 |
---|---|---|
committer | Vikas Kushwaha <dev@vikas.rocks> | 2024-11-21 13:54:38 +0530 |
commit | 6a16bbdcdb40406592e47ee8d489f857837e5c96 (patch) | |
tree | 8d1a9f72115a106657e059f56e5b47df1a92f483 /src |
Initial commit
Diffstat (limited to 'src')
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 }} |