summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrederick Yin <fkfd@macaw.me>2020-07-28 21:46:17 +0800
committerFrederick Yin <fkfd@macaw.me>2020-07-28 21:46:17 +0800
commit0a54df66bb17c053528bfee81436cf8e986ecd69 (patch)
tree748361df0f10512b33fe9f4a0025e8892b786d99
parent3460330d470a45d6a2ce0bc1f02e1eb3de5514f4 (diff)
parent668353172f4874723f16063d08dedc2a9283331e (diff)
Merge branch 'cache' and add dep declarationHEADmaster
-rw-r--r--README.md1
-rw-r--r--git-gmi/config.py4
-rw-r--r--git-gmi/git.py143
3 files changed, 116 insertions, 32 deletions
diff --git a/README.md b/README.md
index 563d437..dea47ef 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,7 @@ Dependencies:
- relatively new version of Python (3.8.3 personally)
- pygit2 (`pip install pygit2`)
- hurry.filesize (`pip install hurry.filesize`)
+- dateutil (`pip install python-dateutil`)
- a gemini server capable of serving CGI
You need to edit the shebang of `git-gmi/cgi`:
diff --git a/git-gmi/config.py b/git-gmi/config.py
index 6624b3b..e72c6f9 100644
--- a/git-gmi/config.py
+++ b/git-gmi/config.py
@@ -2,6 +2,10 @@
GIT_CATALOG = "/home/fakefred/p/_lab/gemini/repos/"
# which path leads to your cgi app after the URL's host part
CGI_PATH = "/cgi/"
+# cache dir
+CACHE_DIR = "/home/fakefred/Archive/_cache/"
+# how long before cache expires, in seconds: int
+CACHE_TTL = 120
# your site's display name
GIT_GMI_SITE_TITLE = "git.gmi demo instance"
# the "main" branch that git.gmi defaults to
diff --git a/git-gmi/git.py b/git-gmi/git.py
index 800dc0f..b758440 100644
--- a/git-gmi/git.py
+++ b/git-gmi/git.py
@@ -1,6 +1,10 @@
from pygit2 import *
from hurry.filesize import size, alternative
-from datetime import datetime
+from datetime import datetime, timedelta
+import dateutil.parser
+from pathlib import Path
+import os
+import shutil
import mimetypes
from const import *
from config import *
@@ -19,12 +23,60 @@ class GitGmiRepo:
def __init__(self, name: str, path: str):
self.name = name
self.path = path
+ self.cache_dir = Path(CACHE_DIR) / name
+ self._init_cache()
try:
self.repo = Repository(path)
except GitError:
raise FileNotFoundError(f"Error: no such repo: {name}")
- def generate_header(self):
+ def _init_cache(self):
+ try:
+ os.mkdir(self.cache_dir)
+ except FileExistsError:
+ pass
+
+ def _read_cache(self, req: list) -> str:
+ # req is what the user requests after the repo name,
+ # like ["tree", "master", "src"]
+ # which points to a file called tree_master_src.gmi
+ # file content:
+ # 20 text/gemini
+ # [body - page content]
+ # [newline]
+ # cached at:
+ # [iso timestamp]
+ fn = "_".join(req) + ".gmi"
+ try:
+ with open(self.cache_dir / fn) as f:
+ response = f.read()
+ f.close()
+ created_at = dateutil.parser.isoparse(response.splitlines()[-1])
+ if datetime.now() - created_at < timedelta(seconds=CACHE_TTL):
+ # cache valid
+ # response will include the timestamp
+ return response
+ except FileNotFoundError:
+ pass
+
+ return None
+
+ def _write_cache(self, req: list, resp: str):
+ # write resp into cache, appended with timestamp
+ fn = "_".join(req) + ".gmi"
+ try:
+ f = open(self.cache_dir / fn, "x")
+ except FileExistsError:
+ f = open(self.cache_dir / fn, "w")
+ f.write(resp + "\ncached at:\n" + datetime.now().isoformat())
+
+ def _flush_cache(self):
+ try:
+ shutil.rmtree(self.cache_dir)
+ except FileNotFoundError:
+ pass
+
+ def _generate_header(self):
# global "header" to display above all views (except raw files)
header = (
f"# {self.name}\n"
@@ -37,9 +89,13 @@ class GitGmiRepo:
return header
def view_summary(self) -> str:
- response = f"{STATUS_SUCCESS} {META_GEMINI}\r\n" + self.generate_header()
+ cached = self._read_cache(["summary"])
+ if cached is not None:
+ return cached
+
+ response = f"{STATUS_SUCCESS} {META_GEMINI}\r\n" + self._generate_header()
# show 3 recent commits
- recent_commits = self.get_commit_log()[:3]
+ recent_commits = self._get_commit_log()[:3]
for cmt in recent_commits:
time = str(datetime.utcfromtimestamp(cmt["time"])) + " UTC"
response += (
@@ -47,8 +103,8 @@ class GitGmiRepo:
f"{cmt['msg'].splitlines()[0]}\n\n"
) # TODO: link to commit view
# find and display readme(.*)
- tree = self.get_tree(MAIN_BRANCH)
- trls = self.list_tree(tree)
+ tree = self._get_tree(MAIN_BRANCH)
+ trls = self._list_tree(tree)
found_readme = False
for item in trls:
if (
@@ -63,9 +119,12 @@ class GitGmiRepo:
)
if not found_readme:
response += "## No readme found."
+
+ self._write_cache(["summary"], response)
+
return response
- def get_commit_log(self) -> list:
+ def _get_commit_log(self) -> list:
# returns useful info from commit log.
repo = self.repo
commits = list(repo.walk(repo[repo.head.target].id, GIT_SORT_TIME))
@@ -83,8 +142,11 @@ class GitGmiRepo:
return log # reverse chronical order
def view_log(self) -> str:
- response = f"{STATUS_SUCCESS} {META_GEMINI}\r\n" + self.generate_header()
- log = self.get_commit_log()
+ cached = self._read_cache(["log"])
+ if cached is not None:
+ return cached
+ response = f"{STATUS_SUCCESS} {META_GEMINI}\r\n" + self._generate_header()
+ log = self._get_commit_log()
for cmt in log:
# looks like "2020-06-06 04:51:21 UTC"
time = str(datetime.utcfromtimestamp(cmt["time"])) + " UTC"
@@ -94,9 +156,10 @@ class GitGmiRepo:
f"=> tree/{cmt['id']}/ view tree\n"
f"{cmt['msg']}\n\n"
)
+ self._write_cache(["log"], response)
return response
- def get_commit(self, commit_str) -> dict:
+ def _get_commit(self, commit_str) -> dict:
try:
commit = self.repo.revparse_single(commit_str)
diff = self.repo.diff(commit.parents[0], commit)
@@ -111,10 +174,13 @@ class GitGmiRepo:
raise FileNotFoundError(f"Error: no such commit: {commit_str}")
def view_commit(self, commit_str) -> str:
- commit = self.get_commit(commit_str)
+ cached = self._read_cache(["commit", commit_str])
+ if cached is not None:
+ return cached
+ commit = self._get_commit(commit_str)
response = (
f"{STATUS_SUCCESS} {META_GEMINI}\r\n"
- + self.generate_header()
+ + self._generate_header()
+ f"{commit['id']} - {commit['author']} - {commit['time']}\n"
+ commit["msg"]
+ "\n"
@@ -124,6 +190,7 @@ class GitGmiRepo:
+ commit["patch"]
+ "\n```"
)
+ self._write_cache(["commit", commit_str], response)
return response
def view_raw_commit(self, commit_str) -> str:
@@ -131,7 +198,7 @@ class GitGmiRepo:
response = f"{STATUS_SUCCESS} {META_PLAINTEXT}\r\n" + commit["patch"]
return response
- def get_refs(self) -> list:
+ def _get_refs(self) -> list:
refs = self.repo.listall_reference_objects()
return [
{
@@ -144,44 +211,48 @@ class GitGmiRepo:
]
def view_refs(self) -> str:
- response = f"{STATUS_SUCCESS} {META_GEMINI}\r\n" + self.generate_header()
- refs = self.get_refs()
+ cached = self._read_cache(["refs"])
+ if cached is not None:
+ return cached
+ response = f"{STATUS_SUCCESS} {META_GEMINI}\r\n" + self._generate_header()
+ refs = self._get_refs()
for ref in refs:
# HACK: filter out refs with slashes as remote branches
if ref["shorthand"].find("/") == -1:
response += (
f"## {ref['shorthand']}\n=> tree/{ref['shorthand']}/ view tree\n\n"
)
+ self._write_cache(["refs"], response)
return response
@classmethod
- def parse_recursive_tree(cls, tree: Tree) -> list:
+ def _parse_recursive_tree(cls, tree: Tree) -> list:
# recursively replace all Trees with a list of Blobs inside it,
# bundled with the Tree's name as a tuple,
# e.g. [('src', [blob0, blob1]), otherblob].
tree_list = list(tree)
for idx, item in enumerate(tree_list):
if isinstance(item, Tree):
- tree_list[idx] = (item.name, cls.parse_recursive_tree(tree_list[idx]))
+ tree_list[idx] = (item.name, cls._parse_recursive_tree(tree_list[idx]))
return tree_list
- def get_tree(self, revision_str: str) -> list:
+ def _get_tree(self, revision_str: str) -> list:
# returns a recursive list of Blob objects
try:
revision = self.repo.revparse_single(revision_str)
if isinstance(revision, Commit):
# top level tree; may contain sub-trees
- return self.parse_recursive_tree(revision.tree)
+ return self._parse_recursive_tree(revision.tree)
elif isinstance(revision, Tag):
- return self.parse_recursive_tree(revision.get_object().tree)
+ return self._parse_recursive_tree(revision.get_object().tree)
except ValueError:
raise FileNotFoundError(f"Error: no such tree: {revision_str}")
return None
@staticmethod
- def list_tree(tree_list: list, location=[]) -> list:
- # tree_list is the output of parse_recursive_tree(<tree>);
+ def _list_tree(tree_list: list, location=[]) -> list:
+ # tree_list is the output of _parse_recursive_tree(<tree>);
# location is which dir you are viewing, represented path-like
# in a list, e.g. ['src', 'static', 'css'] => 'src/static/css',
# which this method will cd into and display to the visitor.
@@ -226,12 +297,16 @@ class GitGmiRepo:
def view_tree(self, branch: str, location=[]) -> str:
# actual Gemini response
# consists of a header and a body
- tree = self.get_tree(branch)
- contents = self.list_tree(tree, location)
+ cached = self._read_cache(["tree", branch] + location)
+ if cached is not None:
+ return cached
+
+ tree = self._get_tree(branch)
+ contents = self._list_tree(tree, location)
items = len(contents)
response = (
f"{STATUS_SUCCESS} {META_GEMINI}\r\n"
- + self.generate_header()
+ + self._generate_header()
+ f"## {self.name}{'/' if location else ''}{'/'.join(location)}/"
f" | {items} {'items' if items > 1 else 'item'}\n\n"
)
@@ -242,15 +317,16 @@ class GitGmiRepo:
)
elif item["type"] == "file":
response += f"=> {item['name']} {item['name']} | {convert_filesize(item['size'])}\n"
+ self._write_cache(["tree", branch] + location, response)
return response
- def get_blob(self, commit_str: str, location=[]) -> Blob:
+ def _get_blob(self, commit_str: str, location=[]) -> Blob:
# returns a specific Blob object
- # location: just like that of list_tree, but the last element
+ # location: just like that of _list_tree, but the last element
# is the filename
try:
- tree = self.get_tree(commit_str)
- trls = self.list_tree(tree, location[:-1])
+ tree = self._get_tree(commit_str)
+ trls = self._list_tree(tree, location[:-1])
for item in trls:
if item["type"] == "file" and item["name"] == location[-1]:
return item["blob"]
@@ -259,10 +335,13 @@ class GitGmiRepo:
raise FileNotFoundError(f"Error: No such tree: {'/'.join(location[:-1])}")
def view_blob(self, branch: str, location=[]) -> str:
- blob = self.get_blob(branch, location)
+ cached = self._read_cache(["tree", branch] + location)
+ if cached is not None:
+ return cached
+ blob = self._get_blob(branch, location)
response = (
f"{STATUS_SUCCESS} {META_GEMINI}\r\n"
- + self.generate_header()
+ + self._generate_header()
+ f"## {self.name}/{'/'.join(location)} | {convert_filesize(blob.size)}\n\n"
)
@@ -284,7 +363,7 @@ class GitGmiRepo:
return response
def view_raw_blob(self, branch: str, location=[]) -> bytes:
- blob = self.get_blob(branch, location)
+ blob = self._get_blob(branch, location)
# if mimetypes can't make out the type, set it to plaintext
guessed_mimetype = mimetypes.guess_type(blob.name)[0] or META_PLAINTEXT
response = bytes(f"{STATUS_SUCCESS} {guessed_mimetype}\r\n", encoding="utf-8")