This repository has been archived on 2024-05-17. You can view files and clone it, but cannot push or open issues or pull requests.
build.py/_src/files.py

125 lines
3.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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