Skip to content

Commit f3fd2f8

Browse files
committed
spool: add common "spool" package
This provides for "spool" files: temporary buffers that are expected to be too big to fit into memory comfortably. This is a pattern that crops up relatively often via copy-pasted code. Signed-off-by: Hank Donnay <hdonnay@redhat.com>
1 parent db75b36 commit f3fd2f8

7 files changed

Lines changed: 422 additions & 1 deletion

File tree

toolkit/go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ module github.com/quay/claircore/toolkit
22

33
go 1.24
44

5-
require github.com/google/go-cmp v0.7.0
5+
require (
6+
github.com/google/go-cmp v0.7.0
7+
golang.org/x/sys v0.37.0
8+
)

toolkit/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
22
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
3+
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
4+
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=

toolkit/spool/os_linux.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package spool
2+
3+
import (
4+
"fmt"
5+
"io/fs"
6+
"os"
7+
"runtime"
8+
"sync"
9+
10+
"golang.org/x/sys/unix"
11+
)
12+
13+
// Init initializes [root].
14+
func init() {
15+
var p string
16+
17+
// If the environment was explicitly set, use it.
18+
var ok bool
19+
if p, ok = os.LookupEnv("TMPDIR"); ok {
20+
goto Open
21+
}
22+
23+
// Try to honor file-hierarchy(7).
24+
for _, name := range []string{`/var/tmp`, os.TempDir()} {
25+
fi, err := os.Stat(name)
26+
if err == nil && fi.IsDir() {
27+
p = name
28+
goto Open
29+
}
30+
}
31+
32+
Open:
33+
var err error
34+
root, err = os.OpenRoot(p)
35+
if err != nil {
36+
panic(err)
37+
}
38+
}
39+
40+
func checkRootTmpFile() bool {
41+
f, err := root.OpenFile(".", os.O_WRONLY|unix.O_TMPFILE, 0o600)
42+
if err != nil {
43+
return false
44+
}
45+
f.Close()
46+
return true
47+
}
48+
49+
var haveTmpFile = sync.OnceValue(checkRootTmpFile)
50+
51+
func osAdjustName(name string) string {
52+
if haveTmpFile() {
53+
return "."
54+
}
55+
return name
56+
}
57+
58+
func osAdjustFlag(flag int) int {
59+
if haveTmpFile() && (flag&os.O_CREATE != 0) {
60+
// If we can use tmp, do so.
61+
flag &= ^os.O_CREATE
62+
flag |= unix.O_TMPFILE
63+
}
64+
return flag
65+
}
66+
67+
func osAddCleanup(f *os.File) {
68+
// If not opened with O_TMPFILE (or there was an error), arrange for the
69+
// file to be removed.
70+
flags, err := unix.FcntlInt(f.Fd(), unix.F_GETFL, 0)
71+
if err != nil || flags&unix.O_TMPFILE == 0 {
72+
runtime.AddCleanup(f, func(name string) { root.Remove(name) }, f.Name())
73+
}
74+
}
75+
76+
// Reopen provides or emulates re-opening a file and obtaining an independent file description.
77+
//
78+
// The Linux implementation reopens files via [magic symlinks] in [proc].
79+
//
80+
// [magic symlinks]: https://www.man7.org/linux/man-pages/man7/symlink.7.html
81+
// [proc]: https://man7.org/linux/man-pages/man5/proc.5.html
82+
func Reopen(f *os.File, flag int) (*os.File, error) {
83+
if flag&os.O_CREATE != 0 {
84+
return nil, fmt.Errorf("spool: cannot pass O_CREATE to Reopen: %w", fs.ErrInvalid)
85+
}
86+
fd := int(f.Fd())
87+
if fd == -1 {
88+
return nil, fs.ErrClosed
89+
}
90+
p := fmt.Sprintf("/proc/self/fd/%d", fd)
91+
92+
return os.OpenFile(p, flag, 0)
93+
}

toolkit/spool/os_unix.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//go:build unix && !linux
2+
3+
package spool
4+
5+
import (
6+
"fmt"
7+
"io/fs"
8+
"os"
9+
"runtime"
10+
)
11+
12+
// Init initializes [root].
13+
func init() {
14+
var p string
15+
16+
// If the environment was explicitly set, use it.
17+
var ok bool
18+
if p, ok = os.LookupEnv("TMPDIR"); ok {
19+
goto Open
20+
}
21+
22+
// Try to honor file-hierarchy(7).
23+
for _, name := range []string{`/var/tmp`, os.TempDir()} {
24+
fi, err := os.Stat(name)
25+
if err == nil && fi.IsDir() {
26+
p = name
27+
goto Open
28+
}
29+
}
30+
31+
Open:
32+
var err error
33+
root, err = os.OpenRoot(p)
34+
if err != nil {
35+
panic(err)
36+
}
37+
}
38+
39+
func osAdjustName(name string) string { return name }
40+
41+
func osAdjustFlag(flag int) int {
42+
return flag & os.O_CREATE
43+
}
44+
45+
func osAddCleanup(f *os.File) {
46+
runtime.AddCleanup(f, func(name string) { root.Remove(name) }, f.Name())
47+
}
48+
49+
// Reopen provides or emulates re-opening a file and obtaining an independent file description.
50+
func Reopen(f *os.File, flag int) (*os.File, error) {
51+
if flag&os.O_CREATE != 0 {
52+
return nil, fmt.Errorf("spool: cannot pass O_CREATE to Reopen: %w", fs.ErrInvalid)
53+
}
54+
return root.OpenFile(f.Name(), flag, 0)
55+
}

toolkit/spool/os_windows.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package spool

toolkit/spool/spool.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Package spool provides utilities for managing "spool files".
2+
//
3+
// Files returned by this package can be counted on to be removed when all open descriptors are closed.
4+
package spool
5+
6+
import (
7+
"cmp"
8+
"io/fs"
9+
"math/rand/v2"
10+
"os"
11+
"runtime"
12+
"strconv"
13+
)
14+
15+
// Root is the location all the spool files go into.
16+
//
17+
// This is initialized in os-specific files.
18+
var root *os.Root
19+
20+
// Mkname generates a unique file name based on the provided prefix.
21+
func mkname(prefix string) string {
22+
return cmp.Or(prefix, "tmp") + "." +
23+
strconv.FormatUint(uint64(rand.Uint32()), 10)
24+
}
25+
26+
// OpenFile opens a temporary file with the provided prefix.
27+
//
28+
// If "prefix" is not provided, "tmp" will be used.
29+
// Returned files cannot be opened by path. Callers should use [Reopen].
30+
func OpenFile(prefix string, flag int, perm fs.FileMode) (*os.File, error) {
31+
name := osAdjustName(mkname(prefix))
32+
flag = osAdjustFlag(flag)
33+
f, err := root.OpenFile(name, flag, perm)
34+
if f != nil {
35+
osAddCleanup(f)
36+
}
37+
return f, err
38+
}
39+
40+
// Create returns an [*os.File] that cannot be opened by path and will be
41+
// removed when closed.
42+
func Create() (*os.File, error) {
43+
return OpenFile("", os.O_CREATE|os.O_RDWR, 0o600)
44+
}
45+
46+
// Mkdir creates a directory with the provided prefix.
47+
//
48+
// The directory will have its contents removed when the returned [*os.Root] is
49+
// garbage collected.
50+
func Mkdir(prefix string, perm fs.FileMode) (*os.Root, error) {
51+
name := mkname(prefix)
52+
if err := root.Mkdir(name, perm); err != nil {
53+
return nil, err
54+
}
55+
r, err := root.OpenRoot(name)
56+
if err == nil { // NB If successful
57+
runtime.AddCleanup(r, func(name string) {
58+
root.RemoveAll(name)
59+
}, name)
60+
}
61+
return r, err
62+
}
63+
64+
/*
65+
This package needs a few parts implemented in OS-specific ways.
66+
Below is a quick rundown of them, along with a ready-to-use documentation comment.
67+
68+
# Exported
69+
70+
The documentation for these implementations should add additional paragraphs explaining the OS-specific parts starting "The ${OS} implementation [...]".
71+
72+
See the Linux implementations in os_linux.go for an example.
73+
74+
// Reopen provides or emulates re-opening a file and obtaining an independent file description.
75+
func Reopen(f *os.File, flag int) (*os.File, error)
76+
77+
The [Reopen] API is not possible with dup(2), which returns another file descriptor to the same file description.
78+
An implementation using dup(2) would not provide independent offsets.
79+
80+
# Unexported
81+
82+
osAdjustName(string) string
83+
84+
AdjustName should modify the passed file name as needed and return the result.
85+
86+
osAdjustFlag(int) int
87+
88+
AdjustFlag should modify the passed flags as needed and return the result.
89+
90+
osAddCleanup(*os.File)
91+
92+
AddCleanup should use [runtime.AddCleanup] to attach any needed cleanup functions to the passed [*os.File].
93+
An implementation will not be called with a nil pointer.
94+
*/

0 commit comments

Comments
 (0)