mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-03 09:10:11 +03:00
100 lines
3.0 KiB
Python
100 lines
3.0 KiB
Python
"""Filesystem primitives shared by image writers and inventory reconciliation."""
|
|
|
|
import asyncio
|
|
import hashlib
|
|
import os
|
|
import stat
|
|
|
|
from gns3server.config import Config
|
|
|
|
|
|
def normalized_path(path):
|
|
return os.path.normcase(os.path.abspath(os.path.expanduser(path)))
|
|
|
|
|
|
def contained_path(path, root):
|
|
try:
|
|
return os.path.commonpath((normalized_path(path), normalized_path(root))) == normalized_path(root)
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
def fingerprint(path):
|
|
info = os.stat(path, follow_symlinks=True)
|
|
if not stat.S_ISREG(info.st_mode):
|
|
raise OSError(f"Not a regular image file: {path}")
|
|
return stat_fingerprint(info)
|
|
|
|
|
|
def stat_fingerprint(info):
|
|
# Store as text: inode/device numbers need not fit a signed SQL BIGINT.
|
|
return ":".join(
|
|
str(value) for value in (info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns)
|
|
)
|
|
|
|
|
|
class ImageLockBusy(Exception):
|
|
pass
|
|
|
|
|
|
class ImageLock:
|
|
"""Advisory lock shared by controller processes using the same config directory.
|
|
|
|
Lock files are deliberately retained: unlinking them permits two processes to
|
|
lock different inodes for the same name. OS locks are released on process exit.
|
|
"""
|
|
|
|
def __init__(self, key, wait=True):
|
|
self.key = key
|
|
self.wait = wait
|
|
self._file = None
|
|
|
|
async def __aenter__(self):
|
|
directory = os.path.join(Config.instance().config_dir, ".image-locks")
|
|
os.makedirs(directory, exist_ok=True)
|
|
path = os.path.join(directory, hashlib.sha256(self.key.encode()).hexdigest() + ".lock")
|
|
self._file = open(path, "a+b")
|
|
if os.name == "nt" and os.fstat(self._file.fileno()).st_size == 0:
|
|
self._file.write(b"\0")
|
|
self._file.flush()
|
|
try:
|
|
while True:
|
|
try:
|
|
if os.name == "nt":
|
|
import msvcrt
|
|
|
|
self._file.seek(0)
|
|
msvcrt.locking(self._file.fileno(), msvcrt.LK_NBLCK, 1)
|
|
else:
|
|
import fcntl
|
|
|
|
fcntl.flock(self._file, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
return self
|
|
except (BlockingIOError, PermissionError):
|
|
if not self.wait:
|
|
raise ImageLockBusy(self.key)
|
|
await asyncio.sleep(0.05)
|
|
except BaseException:
|
|
self._file.close()
|
|
self._file = None
|
|
raise
|
|
|
|
async def __aexit__(self, *args):
|
|
if self._file is not None:
|
|
self._file.close()
|
|
self._file = None
|
|
|
|
|
|
def image_lock(path):
|
|
return ImageLock("image:" + normalized_path(os.path.realpath(path)))
|
|
|
|
|
|
def publish_image(temporary, destination):
|
|
"""Atomically publish a complete file without replacing an existing image.
|
|
|
|
Both paths must be on the same filesystem. Hard linking provides the atomic
|
|
no-overwrite operation that os.replace()/shutil.move() cannot provide.
|
|
"""
|
|
os.link(temporary, destination)
|
|
os.unlink(temporary)
|