164 lines
5.3 KiB
Go
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
|
|
}
|