125 lines
3.9 KiB
Python
125 lines
3.9 KiB
Python
import os
|
||
import sys
|
||
|
||
cache = {} # globally shared object to store file content
|
||
|
||
def read_this(path: str) -> str:
|
||
"""Reads data from the file
|
||
This function uses cache to minimize I/O
|
||
"""
|
||
global cache
|
||
|
||
if path not in cache:
|
||
with open(path) as fp:
|
||
cache[path] = fp.read()
|
||
|
||
return cache[path]
|
||
|
||
def save_this(path: str, content: str):
|
||
"""Writes data to the file
|
||
If the file or intermediary directories does not exist, create them
|
||
This function uses cache to minimize I/O
|
||
"""
|
||
global cache
|
||
|
||
# access control
|
||
if not write_accessible(path, follow_symlinks=False):
|
||
raise PermissionError(f"Attempted to write to {os.path.abspath(path)}")
|
||
|
||
mkdir_this(os.path.dirname(path))
|
||
with open(path, 'w') as fp:
|
||
fp.write(content)
|
||
cache[path] = content
|
||
|
||
def mkdir_this(path: str):
|
||
"""Creates directory and all intermediary directories in the path"""
|
||
|
||
path = os.path.normpath(path)
|
||
to_create = []
|
||
# find non-existing dirs
|
||
while path and not os.path.exists(path):
|
||
to_create.append(path)
|
||
path = os.path.dirname(path)
|
||
to_create.reverse()
|
||
# create them
|
||
for i in to_create:
|
||
os.mkdir(i)
|
||
|
||
def link_this(src: str, dest: str):
|
||
"""Creates symlink to source at destination
|
||
If destination exists, it makes sure it is a (relative) symlink to source –
|
||
if not, exception is being thrown
|
||
"""
|
||
|
||
if not os.path.exists(src):
|
||
raise Exception('Source file does not exist')
|
||
# convert paths to relpaths
|
||
src = os.path.relpath(src, os.getcwd())
|
||
dest = os.path.relpath(dest, os.getcwd())
|
||
|
||
link_src = os.path.relpath(src, start=os.path.dirname(dest))
|
||
if os.path.exists(dest):
|
||
# check if existing destination is legitimate
|
||
if not os.path.islink(dest):
|
||
raise Exception('Destination file is not a symlink')
|
||
if link_src != os.readlink(dest):
|
||
raise Exception(f'Destination file points to different object')
|
||
else:
|
||
mkdir_this(os.path.dirname(dest))
|
||
os.symlink(link_src, dest)
|
||
|
||
def scan_dir(path: str, orig_path=None) -> list:
|
||
"""Iterates recursively through directory and returns list of all files.
|
||
Does not follow symlinks
|
||
"""
|
||
|
||
results = []
|
||
if not orig_path: orig_path = path
|
||
|
||
for entry in os.scandir(path):
|
||
if entry.name.startswith('.'): continue # skip hidden entries (may change in the future)
|
||
# we don't want symlinks right now (smart scanning may be added in the future)
|
||
if entry.is_symlink(): continue
|
||
if entry.is_file():
|
||
results.append(os.path.relpath(entry.path, start=orig_path))
|
||
elif entry.is_dir():
|
||
results += scan_dir(entry.path, orig_path)
|
||
|
||
return results
|
||
|
||
def write_accessible(path: str, follow_symlinks: bool = True) -> bool:
|
||
"""Tells whether the file is in safe area (outside _src, .git, parent dirs)
|
||
Technically, nothing stops build.py from writing there, but to avoid data
|
||
loss as a result of a bug, you should always check what file you're trying
|
||
to delete or write to.
|
||
"""
|
||
|
||
PROTECTED_DIRS = (sys.path[0], '../_src', '../.git')
|
||
|
||
path = os.path.realpath(path) if follow_symlinks else os.path.abspath(path)
|
||
project_root = os.path.dirname(sys.path[0])
|
||
|
||
# protect upper dirs
|
||
if os.path.commonpath((path, project_root)) != project_root: return False
|
||
|
||
# protect lower dirs
|
||
for protected in PROTECTED_DIRS:
|
||
protected = os.path.realpath(protected)
|
||
if os.path.commonpath((path, protected)) == protected:
|
||
return False
|
||
|
||
return True
|
||
|
||
def delete_this(path: str):
|
||
"""Deletes a file if it exists"""
|
||
|
||
if not write_accessible(path, follow_symlinks=False):
|
||
raise PermissionError(f"Attempted to delete {os.path.abspath(path)}")
|
||
|
||
if not os.path.isfile(path): return
|
||
|
||
os.unlink(path)
|
||
print('file deleted!')
|
||
|
||
# TODO: cleanup empty directories
|