Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to store manage media files locally and reference their URLs in Pages #156

Merged
merged 4 commits into from
Aug 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ctfcli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from ctfcli.cli.challenges import ChallengeCommand
from ctfcli.cli.config import ConfigCommand
from ctfcli.cli.instance import InstanceCommand
from ctfcli.cli.media import MediaCommand
from ctfcli.cli.pages import PagesCommand
from ctfcli.cli.plugins import PluginsCommand
from ctfcli.cli.templates import TemplatesCommand
Expand Down Expand Up @@ -111,6 +112,9 @@ def challenge(self):
def pages(self):
return COMMANDS.get("pages")

def media(self):
return COMMANDS.get("media")

def plugins(self):
return COMMANDS.get("plugins")

Expand All @@ -125,6 +129,7 @@ def templates(self):
"plugins": PluginsCommand(),
"templates": TemplatesCommand(),
"instance": InstanceCommand(),
"media": MediaCommand(),
"cli": CTFCLI(),
}

Expand Down
80 changes: 80 additions & 0 deletions ctfcli/cli/media.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import os

import click

from ctfcli.core.api import API
from ctfcli.core.config import Config


class MediaCommand:
def add(self, path):
"""Add local media file to config file and remote instance"""
config = Config()
if config.config.has_section("media") is False:
config.config.add_section("media")

api = API()

new_file = ("file", open(path, mode="rb"))
filename = os.path.basename(path)
location = f"media/{filename}"
file_payload = {
"type": "page",
"location": location,
}

# Specifically use data= here to send multipart/form-data
r = api.post("/api/v1/files", files=[new_file], data=file_payload)
r.raise_for_status()
resp = r.json()
server_location = resp["data"][0]["location"]

# Close the file handle
new_file[1].close()

config.config.set("media", location, f"/files/{server_location}")

with open(config.config_path, "w+") as f:
config.write(f)

def rm(self, path):
"""Remove local media file from remote server and local config"""
config = Config()
api = API()

local_location = config["media"][path]

remote_files = api.get("/api/v1/files?type=page").json()["data"]
for remote_file in remote_files:
if f"/files/{remote_file['location']}" == local_location:
# Delete file from server
r = api.delete(f"/api/v1/files/{remote_file['id']}")
r.raise_for_status()

# Update local config file
del config["media"][path]
with open(config.config_path, "w+") as f:
config.write(f)

def url(self, path):
"""Get server URL for a file key"""
config = Config()
api = API()

if config.config.has_section("media") is False:
config.config.add_section("media")

try:
location = config["media"][path]
except KeyError:
click.secho(f"Could not locate local media '{path}'", fg="red")
return 1

remote_files = api.get("/api/v1/files?type=page").json()["data"]
for remote_file in remote_files:
if f"/files/{remote_file['location']}" == location:
base_url = config["config"]["url"]
base_url = base_url.rstrip("/")
return f"{base_url}{location}"
click.secho(f"Could not locate remote media '{path}'", fg="red")
return 1
15 changes: 15 additions & 0 deletions ctfcli/core/media.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from ctfcli.core.config import Config
from ctfcli.utils.tools import safe_format


class Media:
@staticmethod
def replace_placeholders(content: str) -> str:
config = Config()
try:
section = config["media"]
except KeyError:
section = []
for m in section:
content = safe_format(content, items={m: config["media"][m]})
return content
7 changes: 5 additions & 2 deletions ctfcli/core/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
InvalidPageConfiguration,
InvalidPageFormat,
)
from ctfcli.core.media import Media

PAGE_FORMATS = {
".md": "markdown",
Expand Down Expand Up @@ -85,8 +86,8 @@ def _get_data_by_path(self) -> Optional[Dict]:

with open(self.page_path, "r") as page_file:
page_data = frontmatter.load(page_file)

return {**page_data.metadata, "content": page_data.content}
content = Media.replace_placeholders(page_data.content)
return {**page_data.metadata, "content": content}

def _get_data_by_id(self) -> Optional[Dict]:
r = self.api.get(f"/api/v1/pages/{self.page_id}")
Expand Down Expand Up @@ -173,6 +174,8 @@ def get_format(ext) -> str:

@staticmethod
def get_format_extension(fmt) -> str:
if fmt is None:
return ".md"
for supported_ext, supported_fmt in PAGE_FORMATS.items():
if fmt == supported_fmt:
return supported_ext
Expand Down
9 changes: 9 additions & 0 deletions ctfcli/utils/tools.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
import string


Expand All @@ -21,3 +22,11 @@ def strings(filename, min_length=4):

if len(result) >= min_length: # catch result at EOF
yield result


def safe_format(fmt, items):
"""
Function that safely formats strings with arbitrary potentially user-supplied format strings
Looks for interpolation placeholders like {target} or {{ target }}
"""
return re.sub(r"\{?\{([^{}]*)\}\}?", lambda m: items.get(m.group(1).strip(), m.group(0)), fmt)
Loading