Skip to content

Commit b7a09c1

Browse files
committed
Add ability to store manage media files locally and reference their URLs in Pages
1 parent 8fa7f3d commit b7a09c1

File tree

5 files changed

+85
-2
lines changed

5 files changed

+85
-2
lines changed

ctfcli/__main__.py

+5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ctfcli.cli.challenges import ChallengeCommand
1313
from ctfcli.cli.config import ConfigCommand
1414
from ctfcli.cli.instance import InstanceCommand
15+
from ctfcli.cli.media import MediaCommand
1516
from ctfcli.cli.pages import PagesCommand
1617
from ctfcli.cli.plugins import PluginsCommand
1718
from ctfcli.cli.templates import TemplatesCommand
@@ -111,6 +112,9 @@ def challenge(self):
111112
def pages(self):
112113
return COMMANDS.get("pages")
113114

115+
def media(self):
116+
return COMMANDS.get("media")
117+
114118
def plugins(self):
115119
return COMMANDS.get("plugins")
116120

@@ -125,6 +129,7 @@ def templates(self):
125129
"plugins": PluginsCommand(),
126130
"templates": TemplatesCommand(),
127131
"instance": InstanceCommand(),
132+
"media": MediaCommand(),
128133
"cli": CTFCLI(),
129134
}
130135

ctfcli/cli/media.py

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import os
2+
3+
from ctfcli.core.api import API
4+
from ctfcli.core.config import Config
5+
6+
7+
class MediaCommand:
8+
def add(self, path):
9+
"""Add local media file to config file and remote instance"""
10+
config = Config()
11+
if config.config.has_section("media") is False:
12+
config.config.add_section("media")
13+
14+
api = API()
15+
16+
new_file = ("file", open(path, mode="rb"))
17+
filename = os.path.basename(path)
18+
location = f"media/{filename}"
19+
file_payload = {
20+
"type": "page",
21+
"location": location,
22+
}
23+
24+
# Specifically use data= here to send multipart/form-data
25+
r = api.post("/api/v1/files", files=[new_file], data=file_payload)
26+
r.raise_for_status()
27+
resp = r.json()
28+
server_location = resp["data"][0]["location"]
29+
30+
# Close the file handle
31+
new_file[1].close()
32+
33+
config.config.set("media", location, f"/files/{server_location}")
34+
35+
with open(config.config_path, "w+") as f:
36+
config.write(f)
37+
38+
def rm(self, path):
39+
"""Remove local media file from remote server and local config"""
40+
config = Config()
41+
api = API()
42+
43+
local_location = config["media"][path]
44+
45+
remote_files = api.get("/api/v1/files?type=page").json()["data"]
46+
for remote_file in remote_files:
47+
if f"/files/{remote_file['location']}" == local_location:
48+
# Delete file from server
49+
r = api.delete(f"/api/v1/files/{remote_file['id']}")
50+
r.raise_for_status()
51+
52+
# Update local config file
53+
del config["media"][path]
54+
with open(config.config_path, "w+") as f:
55+
config.write(f)

ctfcli/core/media.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from ctfcli.core.config import Config
2+
from ctfcli.utils.tools import safe_format
3+
4+
5+
class Media:
6+
@staticmethod
7+
def replace_placeholders(content: str) -> str:
8+
config = Config()
9+
for m in config["media"]:
10+
content = safe_format(content, items={m: config["media"][m]})
11+
return content

ctfcli/core/page.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
InvalidPageConfiguration,
1818
InvalidPageFormat,
1919
)
20+
from ctfcli.core.media import Media
2021

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

8687
with open(self.page_path, "r") as page_file:
8788
page_data = frontmatter.load(page_file)
88-
89-
return {**page_data.metadata, "content": page_data.content}
89+
content = Media.replace_placeholders(page_data.content)
90+
return {**page_data.metadata, "content": content}
9091

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

174175
@staticmethod
175176
def get_format_extension(fmt) -> str:
177+
if fmt is None:
178+
return ".md"
176179
for supported_ext, supported_fmt in PAGE_FORMATS.items():
177180
if fmt == supported_fmt:
178181
return supported_ext

ctfcli/utils/tools.py

+9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
import string
23

34

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

2223
if len(result) >= min_length: # catch result at EOF
2324
yield result
25+
26+
27+
def safe_format(fmt, items):
28+
"""
29+
Function that safely formats strings with arbitrary potentially user-supplied format strings
30+
Looks for interpolation placeholders like {target} or {{ target }}
31+
"""
32+
return re.sub(r"\{?\{([^{}]*)\}\}?", lambda m: items.get(m.group(1).strip(), m.group(0)), fmt)

0 commit comments

Comments
 (0)