1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
|
package tempdir
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/containers/storage/internal/staging_lockfile"
"github.com/sirupsen/logrus"
)
/*
Locking rules and invariants for TempDir and its recovery mechanism:
1. TempDir Instance Locks:
- Path: 'RootDir/lock-XYZ' (in the root directory)
- Each TempDir instance creates and holds an exclusive lock on this file immediately
during NewTempDir() initialization.
- This lock signifies that the temporary directory is in active use by the
process/goroutine that holds the TempDir object.
2. Stale Directory Recovery (separate operation):
- RecoverStaleDirs() can be called independently to identify and clean up stale
temporary directories.
- For each potential stale directory (found by listPotentialStaleDirs), it
attempts to TryLockPath() its instance lock file.
- If TryLockPath() succeeds: The directory is considered stale, and both the
directory and lock file are removed.
- If TryLockPath() fails: The directory is considered in active use by another
process/goroutine, and it's skipped.
3. TempDir Usage:
- NewTempDir() immediately creates both the instance lock and the temporary directory.
- TempDir.StageDeletion() moves files into the existing temporary directory with counter-based naming.
- Files moved into the temporary directory are renamed with a counter-based prefix
to ensure uniqueness (e.g., "0-filename", "1-filename").
- Once cleaned up, the TempDir instance cannot be reused - StageDeletion() will return an error.
4. Cleanup Process:
- TempDir.Cleanup() removes both the temporary directory and its lock file.
- The instance lock is unlocked and deleted after cleanup operations are complete.
- The TempDir instance becomes inactive after cleanup (internal fields are reset).
- The TempDir instance cannot be reused after Cleanup() - StageDeletion() will fail.
5. TempDir Lifetime:
- NewTempDir() creates both the TempDir manager and the actual temporary directory immediately.
- The temporary directory is created eagerly during NewTempDir().
- During its lifetime, the temporary directory is protected by its instance lock.
- The temporary directory exists until Cleanup() is called, which removes both
the directory and its lock file.
- Multiple TempDir instances can coexist in the same RootDir, each with its own
unique subdirectory and lock.
- After cleanup, the TempDir instance cannot be reused.
6. Example Directory Structure:
RootDir/
lock-ABC (instance lock for temp-dir-ABC)
temp-dir-ABC/
0-file1
1-file3
lock-XYZ (instance lock for temp-dir-XYZ)
temp-dir-XYZ/
0-file2
*/
const (
// tempDirPrefix is the prefix used for creating temporary directories.
tempDirPrefix = "temp-dir-"
// tempdirLockPrefix is the prefix used for creating lock files for temporary directories.
tempdirLockPrefix = "lock-"
)
// TempDir represents a temporary directory that is created in a specified root directory.
// It manages the lifecycle of the temporary directory, including creation, locking, and cleanup.
// Each TempDir instance is associated with a unique subdirectory in the root directory.
// Warning: The TempDir instance should be used in a single goroutine.
type TempDir struct {
RootDir string
tempDirPath string
// tempDirLock is a lock file (e.g., RootDir/lock-XYZ) specific to this
// TempDir instance, indicating it's in active use.
tempDirLock *staging_lockfile.StagingLockFile
tempDirLockPath string
// counter is used to generate unique filenames for added files.
counter uint64
}
// CleanupTempDirFunc is a function type that can be returned by operations
// which need to perform cleanup actions later.
type CleanupTempDirFunc func() error
// listPotentialStaleDirs scans the RootDir for directories that might be stale temporary directories.
// It identifies directories with the tempDirPrefix and their corresponding lock files with the tempdirLockPrefix.
// The function returns a map of IDs that correspond to both directories and lock files found.
// These IDs are extracted from the filenames by removing their respective prefixes.
func listPotentialStaleDirs(rootDir string) (map[string]struct{}, error) {
ids := make(map[string]struct{})
dirContent, err := os.ReadDir(rootDir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("error reading temp dir %s: %w", rootDir, err)
}
for _, entry := range dirContent {
if id, ok := strings.CutPrefix(entry.Name(), tempDirPrefix); ok {
ids[id] = struct{}{}
continue
}
if id, ok := strings.CutPrefix(entry.Name(), tempdirLockPrefix); ok {
ids[id] = struct{}{}
}
}
return ids, nil
}
// RecoverStaleDirs identifies and removes stale temporary directories in the root directory.
// A directory is considered stale if its lock file can be acquired (indicating no active use).
// The function attempts to remove both the directory and its lock file.
// If a directory's lock cannot be acquired, it is considered in use and is skipped.
func RecoverStaleDirs(rootDir string) error {
potentialStaleDirs, err := listPotentialStaleDirs(rootDir)
if err != nil {
return fmt.Errorf("error listing potential stale temp dirs in %s: %w", rootDir, err)
}
if len(potentialStaleDirs) == 0 {
return nil
}
var recoveryErrors []error
for id := range potentialStaleDirs {
lockPath := filepath.Join(rootDir, tempdirLockPrefix+id)
tempDirPath := filepath.Join(rootDir, tempDirPrefix+id)
// Try to lock the lock file. If it can be locked, the directory is stale.
instanceLock, err := staging_lockfile.TryLockPath(lockPath)
if err != nil {
continue
}
if rmErr := os.RemoveAll(tempDirPath); rmErr != nil && !os.IsNotExist(rmErr) {
recoveryErrors = append(recoveryErrors, fmt.Errorf("error removing stale temp dir %s: %w", tempDirPath, rmErr))
}
if unlockErr := instanceLock.UnlockAndDelete(); unlockErr != nil {
recoveryErrors = append(recoveryErrors, fmt.Errorf("error unlocking and deleting stale lock file %s: %w", lockPath, unlockErr))
}
}
return errors.Join(recoveryErrors...)
}
// NewTempDir creates a TempDir and immediately creates both the temporary directory
// and its corresponding lock file in the specified RootDir.
// The RootDir itself will be created if it doesn't exist.
// Note: The caller MUST ensure that returned TempDir instance is cleaned up with .Cleanup().
func NewTempDir(rootDir string) (*TempDir, error) {
if err := os.MkdirAll(rootDir, 0o700); err != nil {
return nil, fmt.Errorf("creating root temp directory %s failed: %w", rootDir, err)
}
td := &TempDir{
RootDir: rootDir,
}
tempDirLock, tempDirLockFileName, err := staging_lockfile.CreateAndLock(td.RootDir, tempdirLockPrefix)
if err != nil {
return nil, fmt.Errorf("creating and locking temp dir instance lock in %s failed: %w", td.RootDir, err)
}
td.tempDirLock = tempDirLock
td.tempDirLockPath = filepath.Join(td.RootDir, tempDirLockFileName)
// Create the temporary directory that corresponds to the lock file
id := strings.TrimPrefix(tempDirLockFileName, tempdirLockPrefix)
actualTempDirPath := filepath.Join(td.RootDir, tempDirPrefix+id)
if err := os.MkdirAll(actualTempDirPath, 0o700); err != nil {
return nil, fmt.Errorf("creating temp directory %s failed: %w", actualTempDirPath, err)
}
td.tempDirPath = actualTempDirPath
td.counter = 0
return td, nil
}
// StageDeletion moves the specified file into the instance's temporary directory.
// The temporary directory must already exist (created during NewTempDir).
// Files are renamed with a counter-based prefix (e.g., "0-filename", "1-filename") to ensure uniqueness.
// Note: 'path' must be on the same filesystem as the TempDir for os.Rename to work.
// The caller MUST ensure .Cleanup() is called.
// If the TempDir has been cleaned up, this method will return an error.
func (td *TempDir) StageDeletion(path string) error {
if td.tempDirLock == nil {
return fmt.Errorf("temp dir instance not initialized or already cleaned up")
}
fileName := fmt.Sprintf("%d-", td.counter) + filepath.Base(path)
destPath := filepath.Join(td.tempDirPath, fileName)
td.counter++
return os.Rename(path, destPath)
}
// Cleanup removes the temporary directory and releases its instance lock.
// After cleanup, the TempDir instance becomes inactive and cannot be reused.
// Subsequent calls to StageDeletion() will fail.
// Multiple calls to Cleanup() are safe and will not return an error.
// Callers should typically defer Cleanup() to run after any application-level
// global locks are released to avoid holding those locks during potentially
// slow disk I/O.
func (td *TempDir) Cleanup() error {
if td.tempDirLock == nil {
logrus.Debug("Temp dir already cleaned up")
return nil
}
if err := os.RemoveAll(td.tempDirPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("removing temp dir %s failed: %w", td.tempDirPath, err)
}
lock := td.tempDirLock
td.tempDirPath = ""
td.tempDirLock = nil
td.tempDirLockPath = ""
return lock.UnlockAndDelete()
}
// CleanupTemporaryDirectories cleans up multiple temporary directories by calling their cleanup functions.
func CleanupTemporaryDirectories(cleanFuncs ...CleanupTempDirFunc) error {
var cleanupErrors []error
for _, cleanupFunc := range cleanFuncs {
if cleanupFunc == nil {
continue
}
if err := cleanupFunc(); err != nil {
cleanupErrors = append(cleanupErrors, err)
}
}
return errors.Join(cleanupErrors...)
}
|