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)
}
}