From 10eaf13a4eb8c07369edcb8633aca73a32db1201 Mon Sep 17 00:00:00 2001 From: Paul Ferrell Date: Tue, 18 Oct 2016 20:11:20 -0600 Subject: [PATCH 1/3] The devel server now serves puzzle files. --- devel-server.py | 58 +++++++++++++++++++++++++++++++++++++++---------- puzzles.py | 12 +++++----- 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/devel-server.py b/devel-server.py index 5543f83..0f15dfd 100755 --- a/devel-server.py +++ b/devel-server.py @@ -102,35 +102,69 @@ you are a fool. body = [] path = self.path.rstrip('/') parts = path.split("/") + #raise ValueError(parts) if len(parts) < 3: # List all categories body.append("# Puzzle Categories") for i in glob.glob(os.path.join("puzzles", "*", "")): body.append("* [{}](/{})".format(i, i)) - elif len(parts) == 3: + self.serve_md('\n'.join(body)) + return + + fpath = os.path.join("puzzles", parts[2]) + cat = puzzles.Category(fpath, seed) + if len(parts) == 3: # List all point values in a category body.append("# Puzzles in category `{}`".format(parts[2])) - fpath = os.path.join("puzzles", parts[2]) - cat = puzzles.Category(fpath, seed) for points in cat.pointvals: body.append("* [puzzles/{cat}/{points}](/puzzles/{cat}/{points}/)".format(cat=parts[2], points=points)) - elif len(parts) == 4: + self.serve_md('\n'.join(body)) + return + + pzl = cat.puzzle(int(parts[3])) + if len(parts) == 4: body.append("# {} puzzle {}".format(parts[2], parts[3])) - fpath = os.path.join("puzzles", parts[2]) - cat = puzzles.Category(fpath, seed) - p = cat.puzzle(int(parts[3])) - body.append("* Author: `{}`".format(p['author'])) - body.append("* Summary: `{}`".format(p['summary'])) + body.append("* Author: `{}`".format(pzl['author'])) + body.append("* Summary: `{}`".format(pzl['summary'])) body.append('') body.append("## Body") - body.append(p.body) + body.append(pzl.body) body.append("## Answers") - for a in p['answers']: + for a in pzl['answer']: body.append("* `{}`".format(a)) body.append("") + body.append("## Files") + for pzl_file in pzl['files']: + body.append("* [puzzles/{cat}/{points}/{filename}]({filename})" + .format(cat=parts[2], points=pzl.points, filename=pzl_file)) + self.serve_md('\n'.join(body)) + return + elif len(parts) == 5: + try: + self.serve_puzzle_file(pzl['files'][parts[4]]) + except KeyError: + self.send_error(HTTPStatus.NOT_FOUND, "File not found") + return else: body.append("# Not Implemented Yet") - self.serve_md('\n'.join(body)) + self.serve_md('\n'.join(body)) + + CHUNK_SIZE = 4096 + def serve_puzzle_file(self, file): + """Serve a PuzzleFile object.""" + self.send_response(HTTPStatus.OK) + self.send_header("Content-type", "application/octet-stream") + self.send_header('Content-Disposition', 'attachment; filename="{}"'.format(file.name)) + if file.path is not None: + fs = os.stat(file.path) + self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) + + # We're using application/octet stream, so we can send the raw bytes. + self.end_headers() + chunk = file.handle.read(self.CHUNK_SIZE) + while chunk: + self.wfile.write(chunk) + chunk = file.handle.read(self.CHUNK_SIZE) def serve_file(self): if self.path.endswith(".md"): diff --git a/puzzles.py b/puzzles.py index 3923f89..df18c0c 100644 --- a/puzzles.py +++ b/puzzles.py @@ -94,6 +94,9 @@ class Puzzle: self.message = bytes(random.choice(messageChars) for i in range(20)) self.body = '' + # This defaults to a dict, not a list like most things + self._dict['files'] = {} + # A list of temporary files we've created that will need to be deleted. self._temp_files = [] if path is not None: @@ -171,18 +174,17 @@ class Puzzle: # Make sure it actually exists. if not os.path.exists(path): - raise ValueError("Included file {} does not exist.") + raise ValueError("Included file {} does not exist.".format(path)) file = open(path, 'rb') return PuzzleFile(path=path, handle=file, name=name, visible=visible) - def make_temp_file(self, name=None, mode='rw+b', visible=True): + def make_temp_file(self, name=None, visible=True): """Get a file object for adding dynamically generated data to the puzzle. When you're done with this file, flush it, but don't close it. :param name: The name of the file for links within the puzzle. If this is None, a name will be generated for you. - :param mode: The mode under which :param visible: Whether or not the file will be visible to the user. :return: A file object for writing """ @@ -190,7 +192,7 @@ class Puzzle: if name is None: name = self.random_hash() - file = tempfile.NamedTemporaryFile(mode=mode, delete=False) + file = tempfile.NamedTemporaryFile(mode='w+b', delete=False) file_read = open(file.name, 'rb') self._dict['files'][name] = PuzzleFile(path=file.name, handle=file_read, @@ -313,7 +315,7 @@ class Category: def puzzle(self, points): path = os.path.join(self.path, str(points)) - return Puzzle(path, self.seed) + return Puzzle(self.seed, path=path) def puzzles(self): for points in self.pointvals: From 108ec81906a994bdb57ec2d3141369bcf948f207 Mon Sep 17 00:00:00 2001 From: Paul Ferrell Date: Wed, 19 Oct 2016 15:02:38 -0600 Subject: [PATCH 2/3] Added logging support to puzzles, devel-server. Made the puzzle_dir a public member of puzzle. --- devel-server.py | 33 ++++++++++++++++++++++++++++----- puzzles.py | 33 +++++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/devel-server.py b/devel-server.py index 0f15dfd..10133e5 100755 --- a/devel-server.py +++ b/devel-server.py @@ -137,18 +137,41 @@ you are a fool. for pzl_file in pzl['files']: body.append("* [puzzles/{cat}/{points}/{filename}]({filename})" .format(cat=parts[2], points=pzl.points, filename=pzl_file)) + + if len(pzl.logs) > 0: + body.extend(["", "## Logs"]) + body.append("* [Full Log File](_logs)" + .format(cat=parts[2], points=pzl.points)) + body.extend(["", "### Logs Head"]) + for log in pzl.logs[:10]: + body.append("* `{}`".format(log)) + body.extend(["", "### Logs Tail"]) + for log in pzl.logs[-10:]: + body.append("* `{}`".format(log)) self.serve_md('\n'.join(body)) return elif len(parts) == 5: - try: - self.serve_puzzle_file(pzl['files'][parts[4]]) - except KeyError: - self.send_error(HTTPStatus.NOT_FOUND, "File not found") - return + if parts[4] == '_logs': + self.serve_puzzle_logs(pzl.logs) + else: + try: + self.serve_puzzle_file(pzl['files'][parts[4]]) + except KeyError: + self.send_error(HTTPStatus.NOT_FOUND, "File not found") + return else: body.append("# Not Implemented Yet") self.serve_md('\n'.join(body)) + def serve_puzzle_logs(self, logs): + """Serve a PuzzleFile object.""" + self.send_response(HTTPStatus.OK) + self.send_header("Content-type", "text/plain; charset=utf-8") + self.end_headers() + for log in logs: + self.wfile.write(log.encode('ascii')) + self.wfile.write(b"\n") + CHUNK_SIZE = 4096 def serve_puzzle_file(self, file): """Serve a PuzzleFile object.""" diff --git a/puzzles.py b/puzzles.py index df18c0c..2fd5ee7 100644 --- a/puzzles.py +++ b/puzzles.py @@ -88,9 +88,9 @@ class Puzzle: self._dict = defaultdict(lambda: []) if os.path.isdir(path): - self._puzzle_dir = path + self.puzzle_dir = path else: - self._puzzle_dir = None + self.puzzle_dir = None self.message = bytes(random.choice(messageChars) for i in range(20)) self.body = '' @@ -114,6 +114,8 @@ class Puzzle: self._seed = category_seed * self.points self.rand = random.Random(self._seed) + self._logs = [] + if path is not None: files = os.listdir(path) @@ -139,6 +141,25 @@ class Puzzle: except OSError: pass + def log(self, msg): + """Add a new log message to this puzzle.""" + self._logs.append(msg) + + @property + def logs(self): + """Get all the log messages, as strings.""" + + _logs = [] + for log in self._logs: + if type(log) is bytes: + log = log.decode('utf-8') + elif type(log) is not str: + log = str(log) + + _logs.append(log) + + return _logs + def _read_config(self, stream): """Read a configuration file (ISO 2822)""" body = [] @@ -220,7 +241,7 @@ class Puzzle: key = key.lower() - if key in ('file', 'resource', 'hidden') and self._puzzle_dir is None: + if key in ('file', 'resource', 'hidden') and self.puzzle_dir is None: raise KeyError("Cannot set a puzzle file for single file puzzles.") if key == 'answer': @@ -229,15 +250,15 @@ class Puzzle: self._dict['answer'].append(value) elif key == 'file': # Handle adding files to the puzzle - path = os.path.join(self._puzzle_dir, 'files', value) + path = os.path.join(self.puzzle_dir, 'files', value) self._dict['files'][value] = self._puzzle_file(path, value) elif key == 'resource': # Handle adding category files to the puzzle - path = os.path.join(self._puzzle_dir, '../res', value) + path = os.path.join(self.puzzle_dir, '../res', value) self._dict['files'].append(self._puzzle_file(path, value)) elif key == 'hidden': # Handle adding secret, 'hidden' files to the puzzle. - path = os.path.join(self._puzzle_dir, 'files', value) + path = os.path.join(self.puzzle_dir, 'files', value) name = self.random_hash() self._dict['files'].append(self._puzzle_file(path, name, visible=False)) elif key in self.SINGULAR_KEYS: From d8a665e408156e1949bd2c7b621e6f023113e052 Mon Sep 17 00:00:00 2001 From: Paul Ferrell Date: Wed, 19 Oct 2016 15:14:37 -0600 Subject: [PATCH 3/3] Deconflicting for merge. --- puzzles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/puzzles.py b/puzzles.py index 2fd5ee7..ba96896 100644 --- a/puzzles.py +++ b/puzzles.py @@ -336,7 +336,7 @@ class Category: def puzzle(self, points): path = os.path.join(self.path, str(points)) - return Puzzle(self.seed, path=path) + return Puzzle(path, self.seed) def puzzles(self): for points in self.pointvals: