1package bundle23import (4 "bytes"5 "fmt"6 "io"7 "os"8 "path/filepath"9 "sort"10)1112// This file is the whole of the package's filesystem: a walk that turns a13// directory of .atlas files into descriptors, and an install that lets one14// more file in. Everything above it -- the fold, the ordering, the index -- is15// pure, so a host that keeps its library elsewhere reimplements only what is16// here.1718// Extension is the suffix a bundle file carries in a registry directory.19const Extension = ".atlas"2021// Skipped is one candidate a scan could not read. One bad download should not22// take a library down with it, so a scan reports what it passed over and23// leaves the decision -- warn, refuse, ignore -- to its caller. That is also24// what keeps this package free of a logger.25type Skipped struct {26 Locator string27 Err error28}2930func (s Skipped) Error() string { return s.Locator + ": " + s.Err.Error() }31func (s Skipped) Unwrap() error { return s.Err }3233// Scan reads every bundle in dir and returns their descriptors, sorted by34// locator. A directory that is missing or empty is a registry with no35// volumes, not an error.36//37// Only the manifest of each bundle is read, so a scan of a library of38// hundred-megabyte files costs a seek and a small read per file. The returned39// descriptors are what [Fold] and [BuildIndex] consume.40func Scan(dir string) ([]Descriptor, []Skipped, error) {41 paths, err := filepath.Glob(filepath.Join(dir, "*"+Extension))42 if err != nil {43 return nil, nil, fmt.Errorf("scan %s: %w", dir, err)44 }45 sort.Strings(paths)4647 descriptors := make([]Descriptor, 0, len(paths))48 var skipped []Skipped49 for _, path := range paths {50 descriptor, err := Describe(path)51 if err != nil {52 skipped = append(skipped, Skipped{Locator: path, Err: err})53 continue54 }55 descriptors = append(descriptors, descriptor)56 }57 return descriptors, skipped, nil58}5960// Describe opens one bundle far enough to read its manifest and closes it61// again. It is the per-file step of [Scan], exported because a host importing62// a single file wants exactly this and nothing more.63func Describe(path string) (Descriptor, error) {64 reader, err := Open(path)65 if err != nil {66 return Descriptor{}, err67 }68 defer reader.Close()69 return reader.Descriptor(), nil70}7172// Install copies the bundle at source into dir under its versioned name, and73// reports the descriptor of the installed build.74//75// The sequence is deliberate. The source is opened and validated before76// anything is written, so a broken file never lands in a library. The copy77// goes to a temporary name in the same directory and is renamed into place,78// so a reader scanning mid-install sees either no file or a whole one -- the79// temporary name does not end in the bundle extension, so a concurrent scan80// passes over it. The installed file is then reopened and validated again,81// because the copy is what will actually serve and it is the copy's promises82// that matter.83//84// Builds sit side by side and the fold serves the right one, so installing an85// old file can never overwrite a newer build; it just arrives already86// shadowed. Installing a build that is already present is a no-op: the name87// carries the stamp, so a file of that name is that build.88//89// Install touches no application state. Where the host cannot rename over an90// open file, it is the host's business to release its handle and retry.91func Install(dir, source string) (Descriptor, error) {92 opened, err := Open(source)93 if err != nil {94 return Descriptor{}, err95 }96 manifest := opened.Manifest97 if err := opened.Validate(); err != nil {98 opened.Close()99 return Descriptor{}, fmt.Errorf("%s: %w", filepath.Base(source), err)100 }101 opened.Close()102103 target := filepath.Join(dir, VersionedFileName(manifest))104 if sameFile(source, target) {105 return Describe(target)106 }107 if _, err := os.Stat(target); err == nil {108 // This exact build is already installed under its own name.109 return Describe(target)110 }111 if err := copyInto(dir, source, target); err != nil {112 return Descriptor{}, err113 }114 descriptor, err := Describe(target)115 if err != nil {116 return Descriptor{}, fmt.Errorf("installed %s: %w", filepath.Base(target), err)117 }118 installed, err := Open(target)119 if err != nil {120 return Descriptor{}, fmt.Errorf("installed %s: %w", filepath.Base(target), err)121 }122 defer installed.Close()123 if err := installed.Validate(); err != nil {124 return Descriptor{}, fmt.Errorf("installed %s: %w", filepath.Base(target), err)125 }126 return descriptor, nil127}128129// InstallBytes installs a bundle held in memory -- an embedded asset, a130// download -- into dir under its versioned name, and reports the descriptor of131// the installed build. It keeps every promise [Install] makes: the bytes are132// validated before anything is written, the copy stages under a temporary name133// a concurrent scan passes over, an already-present build is a no-op, and the134// installed file is reopened and validated because the copy is what will serve.135func InstallBytes(dir string, source []byte) (Descriptor, error) {136 opened, err := NewReader(bytes.NewReader(source), int64(len(source)))137 if err != nil {138 return Descriptor{}, err139 }140 manifest := opened.Manifest141 if err := opened.Validate(); err != nil {142 return Descriptor{}, err143 }144145 target := filepath.Join(dir, VersionedFileName(manifest))146 if _, err := os.Stat(target); err == nil {147 // This exact build is already installed under its own name.148 return Describe(target)149 }150 staged, err := os.CreateTemp(dir, ".installing-*")151 if err != nil {152 return Descriptor{}, fmt.Errorf("stage %s: %w", filepath.Base(target), err)153 }154 defer os.Remove(staged.Name())155 if _, err := staged.Write(source); err != nil {156 staged.Close()157 return Descriptor{}, fmt.Errorf("stage %s: %w", filepath.Base(target), err)158 }159 if err := staged.Close(); err != nil {160 return Descriptor{}, fmt.Errorf("stage %s: %w", filepath.Base(target), err)161 }162 // CreateTemp opens private files; a bundle is an artifact anyone may copy.163 if err := os.Chmod(staged.Name(), 0o644); err != nil {164 return Descriptor{}, fmt.Errorf("stage %s: %w", filepath.Base(target), err)165 }166 if err := os.Rename(staged.Name(), target); err != nil {167 return Descriptor{}, fmt.Errorf("install %s: %w", filepath.Base(target), err)168 }169 descriptor, err := Describe(target)170 if err != nil {171 return Descriptor{}, fmt.Errorf("installed %s: %w", filepath.Base(target), err)172 }173 installed, err := Open(target)174 if err != nil {175 return Descriptor{}, fmt.Errorf("installed %s: %w", filepath.Base(target), err)176 }177 defer installed.Close()178 if err := installed.Validate(); err != nil {179 return Descriptor{}, fmt.Errorf("installed %s: %w", filepath.Base(target), err)180 }181 return descriptor, nil182}183184// copyInto stages a copy of source beside target and renames it into place.185func copyInto(dir, source, target string) error {186 from, err := os.Open(source)187 if err != nil {188 return fmt.Errorf("open %s: %w", filepath.Base(source), err)189 }190 defer from.Close()191192 staged, err := os.CreateTemp(dir, ".installing-*")193 if err != nil {194 return fmt.Errorf("stage %s: %w", filepath.Base(source), err)195 }196 defer os.Remove(staged.Name())197 if _, err := io.Copy(staged, from); err != nil {198 staged.Close()199 return fmt.Errorf("copy %s: %w", filepath.Base(source), err)200 }201 if err := staged.Close(); err != nil {202 return fmt.Errorf("copy %s: %w", filepath.Base(source), err)203 }204 // CreateTemp opens private files; a bundle is an artifact anyone may copy.205 if err := os.Chmod(staged.Name(), 0o644); err != nil {206 return fmt.Errorf("stage %s: %w", filepath.Base(source), err)207 }208 if err := os.Rename(staged.Name(), target); err != nil {209 return fmt.Errorf("install %s: %w", filepath.Base(target), err)210 }211 return nil212}213214// WriteIndex derives the listing from descriptors and writes index.json215// beside them. It is the only thing this package writes into a registry216// directory that is not a bundle, and it is always safe to delete: the next217// call derives it again.218func WriteIndex(dir string, candidates []Descriptor) error {219 data, err := MarshalIndex(BuildIndex(candidates))220 if err != nil {221 return err222 }223 path := filepath.Join(dir, "index.json")224 if err := os.WriteFile(path, data, 0o644); err != nil {225 return fmt.Errorf("write %s: %w", path, err)226 }227 return nil228}229230// sameFile reports whether two paths name one file, so installing a bundle231// that already sits in the library under its own name copies nothing.232func sameFile(a, b string) bool {233 left, err := os.Stat(a)234 if err != nil {235 return false236 }237 right, err := os.Stat(b)238 if err != nil {239 return false240 }241 return os.SameFile(left, right)242}