Files
mesh-catalog/modules/brother/cmd/brother/file.go
T

164 lines
5.3 KiB
Go

package main
import (
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
// FileMode is the mode a created scan folder gets and ScanMode the mode of a filed scan.
const (
FolderMode os.FileMode = 0o755
ScanMode os.FileMode = 0o644
)
// File writes the PDF into the folder as scan-YYYY-MM-DD-HHMMSS.pdf (local time): whole beside its place as
// .scan-*.tmp and then renamed, so nothing ever sees half a scan. A name taken already gets -2, -3, and a file
// that is there is never overwritten. The folder is created when it is not there. Nothing in it that this
// writes is touched but what it makes (1440/AC5, AC11).
func File(folder string, pdf []byte, at time.Time) (string, error) {
if !filepath.IsAbs(folder) {
return "", fmt.Errorf("scan-folder %q is not an absolute path", folder)
}
if _, err := os.Stat(folder); os.IsNotExist(err) {
if err := os.MkdirAll(folder, FolderMode); err != nil {
return "", fmt.Errorf("the scan folder cannot be made: %w", err)
}
// The umask may have narrowed it; a folder this made is 0755 whatever the umask.
if err := os.Chmod(folder, FolderMode); err != nil {
return "", fmt.Errorf("the scan folder cannot be made: %w", err)
}
}
tmp, err := os.CreateTemp(folder, ".scan-*.tmp")
if err != nil {
return "", fmt.Errorf("the scan cannot be written in %s: %w", folder, err)
}
done := false
defer func() {
if !done {
tmp.Close()
os.Remove(tmp.Name())
}
}()
if _, err := tmp.Write(pdf); err != nil {
return "", fmt.Errorf("the scan cannot be written: %w", err)
}
if err := tmp.Sync(); err != nil {
return "", fmt.Errorf("the scan cannot be written: %w", err)
}
if err := tmp.Close(); err != nil {
return "", fmt.Errorf("the scan cannot be written: %w", err)
}
if err := os.Chmod(tmp.Name(), ScanMode); err != nil {
return "", err
}
stem := "scan-" + at.Local().Format("2006-01-02-150405")
for n := 1; n < 10000; n++ {
name := stem + ".pdf"
if n > 1 {
name = fmt.Sprintf("%s-%d.pdf", stem, n)
}
final := filepath.Join(folder, name)
// The name is taken by creating it exclusively; the finished file is then renamed over that empty
// one, so a scan never replaces a file that was there.
f, err := os.OpenFile(final, os.O_CREATE|os.O_EXCL|os.O_WRONLY, ScanMode)
if os.IsExist(err) {
continue
}
if err != nil {
return "", fmt.Errorf("the scan cannot be named in %s: %w", folder, err)
}
f.Close()
if err := os.Rename(tmp.Name(), final); err != nil {
os.Remove(final)
return "", fmt.Errorf("the scan cannot be put in place: %w", err)
}
done = true
return final, nil
}
return "", fmt.Errorf("no free scan name for %s in %s", stem, folder)
}
// Entry is one filed scan, as scan_list says it.
type Entry struct {
Path string `json:"path"`
Bytes int64 `json:"bytes"`
Modified time.Time `json:"modified"`
}
// List is the newest PDFs named scan-*.pdf in the folder, newest first, at most limit. A folder that is not
// there yet is an empty list.
func List(folder string, limit int) ([]Entry, error) {
entries, err := os.ReadDir(folder)
if os.IsNotExist(err) {
return []Entry{}, nil
}
if err != nil {
return nil, fmt.Errorf("the scan folder cannot be read: %w", err)
}
out := []Entry{}
for _, e := range entries {
n := e.Name()
if e.IsDir() || !strings.HasPrefix(n, "scan-") || !strings.HasSuffix(n, ".pdf") {
continue
}
info, err := e.Info()
if err != nil || !info.Mode().IsRegular() {
continue
}
out = append(out, Entry{Path: filepath.Join(folder, n), Bytes: info.Size(), Modified: info.ModTime()})
}
sort.SliceStable(out, func(i, j int) bool {
if !out[i].Modified.Equal(out[j].Modified) {
return out[i].Modified.After(out[j].Modified)
}
return out[i].Path > out[j].Path
})
if limit > 0 && len(out) > limit {
out = out[:limit]
}
return out, nil
}
// PathRefused is the one answer to a path that may not be printed from: it says nothing of whether the path exists,
// what it is or how large (issue 1551).
const PathRefused = "path refused: only a file under the home folder"
// ReadDocument is the file at path, read whole: a regular file under the tool account's home folder, of at most
// most bytes. The path is resolved through its symbolic links first and the real path must still lie under the
// home folder (which is itself resolved); anything else, a missing path included, is PathRefused and no more.
func ReadDocument(path string, most int64) ([]byte, error) {
home, err := os.UserHomeDir()
if err != nil || home == "" || !filepath.IsAbs(path) {
return nil, errors.New(PathRefused)
}
realHome, err := filepath.EvalSymlinks(home)
if err != nil {
return nil, errors.New(PathRefused)
}
real, err := filepath.EvalSymlinks(path)
if err != nil {
return nil, errors.New(PathRefused)
}
rel, err := filepath.Rel(realHome, real)
if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return nil, errors.New(PathRefused)
}
info, err := os.Stat(real)
if err != nil || !info.Mode().IsRegular() {
return nil, errors.New(PathRefused)
}
if info.Size() > most {
return nil, fmt.Errorf("file %s is %d bytes, over the %d the module takes", path, info.Size(), most)
}
doc, err := os.ReadFile(real)
if err != nil {
return nil, fmt.Errorf("file %s cannot be read: %w", path, err)
}
return doc, nil
}