blobs

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package blobs is the filesystem half of a library: its folder, and the papers/ folder inside it where PDFs live (ADR-011, ADR-012).

D4 named each PDF by its SHA-256. D18 replaced that with readable names, one folder per work — Piwowar-2018-The-state-of-OA--F7K2M9QXA/published.pdf — and moved the hash into the database, where it still deduplicates and still verifies. This package knows the names, the layout and the bytes; it knows nothing about SQL. What a file belongs to is internal/store's business, and reconciling the two is internal/library's.

Two rules hold everywhere here:

  • The researcher owns the tree. fil writes new folders and files, and renames only what it named; it never moves, edits or deletes anything a person put there.
  • Writes are atomic. A file is written under .filiation/tmp/, synced, checked and only then renamed into place, so a crash leaves at most a temporary file — never a bad PDF under a good name.

Index

Constants

View Source
const (
	PapersDir  = "papers"
	InboxDir   = "Inbox"
	ExportsDir = "exports"
)

The parts of a library folder (ADR-011).

View Source
const (

	// LongPath is the length past which fil warns that a path is close to
	// Windows' 260-character limit, which some tools still enforce.
	LongPath = 240
)
View Source
const ReportName = "fil-report.txt"

ReportName is the note fil leaves in Inbox/ saying why each file still there was not filed, so a researcher who only looks at the folder still finds out.

Variables

View Source
var ErrNotPDF = errors.New("not a PDF")

ErrNotPDF is returned when bytes meant to be a PDF are not one: a publisher's HTML landing page served under a .pdf link is the usual case.

View Source
var ErrTooLarge = errors.New("larger than the size limit")

ErrTooLarge is returned when bytes run past WriteOptions.MaxSize.

Functions

func Base

func Base(rel string) string

Base is the last element of a stored path.

func Clean

func Clean(rel string) string

Clean turns a path relative to the library folder into the form the database stores: forward slashes, no leading ./, and Unicode NFC. macOS stores accented names decomposed, so without NFC the same folder would compare unequal on different machines.

func Dir

func Dir(rel string) string

Dir is the folder a stored path is in, "." for one at the top.

func FileVersion

func FileVersion(name string) string

FileVersion maps a file name fil gave to the version of the paper it holds, in OpenAlex's vocabulary, or "" for any other name.

func FolderID

func FolderID(name string) (string, bool)

FolderID returns the fil ID at the end of a folder's name, normalised, and whether there is one. It accepts any letter case, because a person may retype a name and some file systems do not keep case.

func FolderName

func FolderName(w model.Work) string

FolderName is the name fil gives a work's folder: {FirstAuthorSurname}-{Year}-{ShortTitle}--{filID}.

Accents are folded to ASCII and everything but letters and digits is dropped, so no character Windows forbids can survive, and a name ending in a fil ID is never a reserved one like CON or NUL. A work with no author gives Unknown, with no year nd, and with no title — or one in a script with no ASCII letters — Untitled. The fil ID at the end keeps names unique on case-insensitive file systems and lets a scan find a folder the researcher moved.

func IsPDF

func IsPDF(b []byte) bool

IsPDF reports whether b, the start of a file, is a PDF. The header may follow a little junk — the specification allows it within the first 1024 bytes, and real files take advantage.

Types

type Entry

type Entry struct {
	Path    string // relative to the library folder
	Size    int64
	ModTime time.Time
}

Entry is one file.

func (Entry) IsPDF

func (e Entry) IsPDF() bool

IsPDF reports whether the entry is named as a PDF.

type File

type File struct {
	Path   string // relative to the library folder, as stored
	SHA256 string // hex
	Size   int64
}

File is a file fil has written or read: where it is, and what is in it.

type Folder

type Folder struct {
	// contains filtered or unexported fields
}

Folder is a library folder on disk.

func Open

func Open(root string) (*Folder, error)

Open prepares the library folder at root: it creates papers/, Inbox/, exports/ and the temporary directory if they are missing, and clears temporary files a crash left behind. It creates nothing else, and touches nothing the researcher put there.

func (*Folder) Abs

func (f *Folder) Abs(rel string) string

Abs turns a stored path, relative to the library folder, into a real one.

func (*Folder) Hash

func (f *Folder) Hash(rel string) (File, error)

Hash reads the file at rel and returns its hash and size.

func (*Folder) Inbox

func (f *Folder) Inbox() ([]Entry, error)

Inbox lists the PDFs in Inbox/, at any depth, oldest first by path. Hidden files and system litter are skipped, as in papers/.

func (*Folder) MakeWorkFolder

func (f *Folder) MakeWorkFolder(name string) (string, error)

MakeWorkFolder creates papers/<name> and returns its stored path. A folder already there is used as it is.

func (*Folder) MoveInto

func (f *Folder) MoveInto(rel, dir string) (string, error)

MoveInto moves the file at rel into the folder dir, keeping its name — or "name-2.pdf" and on if the name is taken: nothing is ever overwritten. It returns the new path. Both must be inside the library folder, so the move is a rename on one volume.

func (*Folder) Rel

func (f *Folder) Rel(abs string) (string, error)

Rel turns a real path inside the library folder into the stored form.

func (*Folder) RenameFolder

func (f *Folder) RenameFolder(rel, name string) (string, error)

RenameFolder moves a folder fil named to a new name in the same parent, and returns the new stored path. It refuses to overwrite.

func (*Folder) Root

func (f *Folder) Root() string

Root is the library folder.

func (*Folder) Stage

func (f *Folder) Stage(r io.Reader, opts WriteOptions) (*Staged, error)

Stage writes r to a temporary file in the library folder, syncs it and checks it. On any failure nothing is left behind.

func (*Folder) Walk

func (f *Folder) Walk() (Listing, error)

Walk lists papers/. It reads directory entries only — no file is opened — so it stays well under a second for tens of thousands of folders.

Hidden files and folders, and the litter operating systems leave behind, are skipped. Symbolic links are not followed. Everything inside a work folder belongs to that work, at any depth; a work folder inside another work folder is treated as part of the outer one.

func (*Folder) Write

func (f *Folder) Write(rel string, r io.Reader, opts WriteOptions) (File, error)

Write stores r at rel atomically: Stage, then Commit. Any failure leaves rel untouched and no temporary file behind. Write refuses to replace a file that is already there — whatever is at rel is either fil's own earlier copy or the researcher's, and neither is for overwriting.

func (*Folder) WriteReport

func (f *Folder) WriteReport(text string) error

WriteReport replaces Inbox/fil-report.txt with text, or removes it when text is empty. It is fil's own file, so replacing it is allowed.

type Listing

type Listing struct {
	// WorkFolders are folders whose name ends in a fil ID, wherever they sit.
	WorkFolders []WorkFolder
	// Loose are files that are not inside a work folder: PDFs dropped in by
	// the researcher, or the contents of a work folder whose fil ID was taken
	// off its name.
	Loose []Entry
	// Folders are the researcher's own folders — every folder that is not a
	// work folder or inside one — relative to the library folder.
	Folders []string
}

Listing is what is in papers/ right now, before anyone has decided what any of it means.

type Staged

type Staged struct {
	SHA256 string
	Size   int64
	// contains filtered or unexported fields
}

Staged is a file written to the library's temporary directory, checked, and not yet in place. Its hash is known, so a caller can decide whether the bytes are wanted — a download fil already has, say — before they land anywhere a person would see them. Every Staged must end in Commit or Discard.

func (*Staged) Commit

func (s *Staged) Commit(rel string) (File, error)

Commit moves the staged file to rel, refusing to replace anything there. The check and the rename are not one step, but only fil writes through here and only into folders it named.

func (*Staged) Discard

func (s *Staged) Discard()

Discard removes the staged file. After a Commit it does nothing.

type WorkFolder

type WorkFolder struct {
	Path  string // relative to the library folder
	FilID string // as written in the name, normalised; may be an alias
	Files []Entry
}

WorkFolder is a folder named for one work.

type WriteOptions

type WriteOptions struct {
	// PDF requires the bytes to start like a PDF.
	PDF bool
	// SHA256, when set, is the hash the bytes must have.
	SHA256 string
	// MaxSize stops a runaway download. Zero means no limit.
	MaxSize int64
}

WriteOptions control Write.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL