Repository navigation
Expand file tree
/
Copy pathworkspace_storage.py
More file actions
89 lines (77 loc) · 3 KB
/
Copy pathworkspace_storage.py
File metadata and controls
89 lines (77 loc) · 3 KB
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
"""Owned disposable workspaces; legacy and live directories are never reclaimed."""
import json
import logging
from pathlib import Path
import shutil
from service_files import ExclusiveFileLock, FileLockUnavailable
MARKER = '.spiral-workspace.json'
METADATA = {'kind': 'spiral-editing-workspace', 'version': 1}
def storage_lock(parent):
return ExclusiveFileLock(parent / '.reclamation.lock').acquire(timeout=15)
def owner_lock_path(root):
# Keep the lock inode stable even if directory deletion fails at the last step.
return root.parent / f'.{root.name}.owner.lock'
def remove_workspace(root):
"""Keep ownership metadata until all disposable contents have been removed."""
try:
for child in root.iterdir():
if child.name == MARKER:
continue
if child.is_dir() and not child.is_symlink():
shutil.rmtree(child)
else:
child.unlink()
# The caller holds the parent lock throughout deletion.
(root / MARKER).unlink()
try:
root.rmdir()
except OSError:
(root / MARKER).write_text(json.dumps(METADATA))
raise
except OSError as exc:
if isinstance(exc, FileNotFoundError) and not root.exists():
return
logging.exception('Failed to clean Spiral workspace %s', root)
raise
def create_workspace(root):
with storage_lock(root.parent):
root.mkdir()
owner = ExclusiveFileLock(owner_lock_path(root)).acquire()
try:
(root / MARKER).write_text(json.dumps(METADATA))
except BaseException:
owner.release()
raise
return owner
def reclaim_workspaces(output):
parent = Path(output) / 'editing-workspaces'
if parent.is_symlink():
return
with storage_lock(parent):
for root in parent.iterdir():
if root.is_symlink() or not root.is_dir():
continue
marker, lock = root / MARKER, owner_lock_path(root)
if marker.is_symlink() or lock.is_symlink() or not lock.is_file():
continue
try:
metadata = json.loads(marker.read_text())
if metadata != METADATA or type(metadata['version']) is not int:
continue
except (OSError, ValueError):
continue
try:
owner = ExclusiveFileLock(lock).acquire()
except FileLockUnavailable:
continue
try:
remove_workspace(root)
# Release before unlink: an open lock handle holds its
# file on Windows, and storage_lock fences new claimants.
owner.release()
lock.unlink()
except OSError:
logging.exception('Failed to reclaim Spiral workspace %s (lock %s)', root, lock)
finally:
# Idempotent: a no-op on the success path, a safety net otherwise.
owner.release()