diff --git a/src/gitcloak/app.py b/src/gitcloak/app.py index 0a85373..8bc5e2a 100644 --- a/src/gitcloak/app.py +++ b/src/gitcloak/app.py @@ -1,6 +1,6 @@ from flask import Flask, render_template, abort, send_from_directory from .classes.git import Git -import markdown2 +from .classes.markdown import RelativeURLRewriter import logging from pathlib import Path @@ -14,6 +14,11 @@ logger.addHandler(handler) app = Flask(__name__) +@app.route("/") +def index(): + return render_template("index.html") + + @app.route("/assets/") def send_assets(path): return send_from_directory(Path(__file__).parent / "assets", path) @@ -29,19 +34,23 @@ def get_tree(owner, repo, path=""): repo_url = f"https://github.com/{owner}/{repo}.git" git = Git(repo_url) try: - directory_structure = git.get_directory_structure() - filtered_structure = [ - entry[len(path) :].lstrip("/") - for entry in directory_structure - if entry.startswith(path) - ] + directory_structure = git.get_directory_structure(path) + filtered_structure = directory_structure logger.debug(f"Filtered structure: {filtered_structure}") # Separate files and directories - directories = sorted(list(set( - [entry.split("/")[0] for entry in filtered_structure if "/" in entry] - ))) + directories = sorted( + list( + set( + [ + entry.split("/")[0] + for entry in filtered_structure + if "/" in entry + ] + ) + ) + ) files = [entry for entry in filtered_structure if "/" not in entry] # Get README.md content if it exists @@ -49,7 +58,10 @@ def get_tree(owner, repo, path=""): if f"README.md" in files: readme_md = git.get_file_content(f"{path}/README.md") - readme_content = markdown2.markdown(readme_md.decode("utf-8")) + base_url = f"/{owner}/{repo}/raw/main/{path}".rstrip("/") + readme_content = RelativeURLRewriter(base_url).convert( + readme_md.decode("utf-8") + ) return render_template( "path.html", @@ -67,7 +79,7 @@ def get_tree(owner, repo, path=""): abort(404, description=str(e)) -@app.route("///raw/", methods=["GET"]) +@app.route("///raw/main/", methods=["GET"]) def get_raw(owner, repo, file_path): repo_url = f"https://github.com/{owner}/{repo}.git" git = Git(repo_url) diff --git a/src/gitcloak/assets/css/style.css b/src/gitcloak/assets/css/style.css index 5ec9c79..7ba55f4 100644 --- a/src/gitcloak/assets/css/style.css +++ b/src/gitcloak/assets/css/style.css @@ -5,6 +5,15 @@ body { height: 100%; } +body { + display: flex; + flex-direction: column; +} + +.container { + flex: 1; +} + .directory a, .file a { text-decoration: none; @@ -33,6 +42,9 @@ body { .icon { margin-right: 5px; + width: 1em; + height: 1em; + vertical-align: middle; } .list-group-item { @@ -43,4 +55,20 @@ body { .navbar-brand { font-size: 1.5rem; padding: 0 10px; +} + +.footer { + position: fixed; + bottom: 0; + width: 100%; + text-align: center; +} + +.text-white { + color: white; +} + +.text-white a { + color: white; + text-decoration: underline; } \ No newline at end of file diff --git a/src/gitcloak/classes/git.py b/src/gitcloak/classes/git.py index bab038a..95453c1 100644 --- a/src/gitcloak/classes/git.py +++ b/src/gitcloak/classes/git.py @@ -1,26 +1,67 @@ import requests -from dulwich.objects import Tree, Blob + +import logging + +from dulwich.objects import Tree, Blob, ShaFile, Tree from dulwich.client import HttpGitClient, get_transport_and_path from dulwich.repo import MemoryRepo class InMemoryRepo(MemoryRepo): - def get_tree(self, commit_sha): + def get_tree(self, commit_sha: bytes) -> Tree: + """Return the tree object for the given commit. + + Args: + commit_sha (bytes): The commit hash. + + Returns: + ShaFile: The tree object. + """ commit = self.get_object(commit_sha) return self.get_object(commit.tree) - def list_tree(self, tree, prefix=""): + def list_tree(self, tree, path="", prefix=""): + """List the directory structure of the tree object. + + Args: + tree (Tree): The tree object. + path (str): The path within the tree object. + prefix (str): The prefix to be added to the path. + + Yields: + str: The path of the file or directory. + """ + logging.debug(f"Listing tree {tree.sha()} with path {path}, prefix {prefix}") + for entry in tree.items(): - entry_path = f"{prefix}/{entry.path.decode('utf-8')}" if prefix else entry.path.decode('utf-8') + entry_path = ( + f"{prefix}/{entry.path.decode('utf-8')}" + if prefix + else entry.path.decode("utf-8") + ) + + if path: + path_parts = path.split("/") + if path_parts[0] != entry.path.decode("utf-8"): + continue + if isinstance(self.get_object(entry.sha), Tree): - for _ in self.list_tree(self.get_object(entry.sha), entry_path): - yield(_) + if path: + for _ in self.list_tree( + self.get_object(entry.sha), path="/".join(path_parts[1:]), prefix="/".join(path_parts[1:]) + ): + yield (_) + else: + for _ in self.list_tree( + self.get_object(entry.sha), prefix=entry_path + ): + yield (_) else: - yield(entry_path) + yield (entry_path) def get_file_content(self, tree, file_path): - parts = file_path.split('/') + parts = file_path.split("/") for entry in tree.items(): - entry_name = entry.path.decode('utf-8') + entry_name = entry.path.decode("utf-8") if entry_name == parts[0]: if len(parts) == 1: file_obj = self.get_object(entry.sha) @@ -30,14 +71,17 @@ class InMemoryRepo(MemoryRepo): raise ValueError(f"Path {file_path} is not a file.") else: if isinstance(self.get_object(entry.sha), Tree): - return self.get_file_content(self.get_object(entry.sha), '/'.join(parts[1:])) + return self.get_file_content( + self.get_object(entry.sha), "/".join(parts[1:]) + ) else: raise ValueError(f"Path {file_path} is not a directory.") raise ValueError(f"File {file_path} not found in the repository.") + class Git: def __init__(self, repo_url): - self.repo_url = repo_url.rstrip('/') + self.repo_url = repo_url.rstrip("/") self.client = HttpGitClient(self.repo_url) def get_remote_refs(self): @@ -46,16 +90,16 @@ class Git: return refs def get_head_commit(self, refs): - return refs[b'HEAD'] + return refs[b"HEAD"] def get_pack_data(self, commit_sha): url = f"{self.repo_url}/git-upload-pack" request_body = f"0032want {commit_sha} multi_ack_detailed side-band-64k thin-pack ofs-delta agent=git/2.28.0\n00000009done\n" - response = requests.post(url, data=request_body.encode('utf-8')) + response = requests.post(url, data=request_body.encode("utf-8")) response.raise_for_status() return response.content - def get_directory_structure(self): + def get_directory_structure(self, path=""): # Initialize an in-memory repository self.repo = InMemoryRepo() @@ -67,10 +111,10 @@ class Git: tree = self.repo.get_tree(head_commit_hash) # List the directory structure - return list(self.repo.list_tree(tree)) + return list(self.repo.list_tree(tree, path=path)) def get_file_content(self, file_path): - file_path = file_path.lstrip('/') + file_path = file_path.lstrip("/") # Initialize an in-memory repository self.repo = InMemoryRepo() @@ -83,4 +127,4 @@ class Git: tree = self.repo.get_tree(head_commit_hash) # Get the file content - return self.repo.get_file_content(tree, file_path) \ No newline at end of file + return self.repo.get_file_content(tree, file_path) diff --git a/src/gitcloak/classes/markdown.py b/src/gitcloak/classes/markdown.py new file mode 100644 index 0000000..024c78b --- /dev/null +++ b/src/gitcloak/classes/markdown.py @@ -0,0 +1,20 @@ +import re +import markdown2 + +class RelativeURLRewriter(markdown2.Markdown): + def __init__(self, base_url, *args, **kwargs): + self.base_url = base_url + super().__init__(*args, **kwargs) + + def postprocess(self, text): + # Rewrite relative URLs + def replace_url(match): + url = match.group(1) + print(f"URL: {match}") + if not (":" in url or url.startswith("/") or url.startswith("#") or url.startswith("md5-")): + return f'src="{self.base_url}/{url}"' + return match.group(0) + + text = re.sub(r'src="([^"]+)"', replace_url, text) + text = re.sub(r'href="([^"]+)"', replace_url, text) + return text \ No newline at end of file diff --git a/src/gitcloak/templates/base.html b/src/gitcloak/templates/base.html index 83d7676..8640b5c 100644 --- a/src/gitcloak/templates/base.html +++ b/src/gitcloak/templates/base.html @@ -8,11 +8,16 @@ -