diff --git a/ctfcli/cli/media.py b/ctfcli/cli/media.py index 7dc14ec..4363b31 100644 --- a/ctfcli/cli/media.py +++ b/ctfcli/cli/media.py @@ -4,6 +4,8 @@ from ctfcli.core.api import API from ctfcli.core.config import Config +from ctfcli.core.media import Media +from ctfcli.utils.hashing import hash_file class MediaCommand: @@ -17,23 +19,57 @@ def add(self, path): filename = os.path.basename(path) location = f"media/{filename}" - file_payload = { - "type": "page", - "location": location, - } - - with open(path, mode="rb") as file_handle: - # Specifically use data= here to send multipart/form-data - r = api.post("/api/v1/files", files={"file": (filename, file_handle)}, data=file_payload) - r.raise_for_status() - resp = r.json() - server_location = resp["data"][0]["location"] + server_location = Media.upload(api, path, location) config.config.set("media", location, f"/files/{server_location}") with open(config.config_path, "w+") as f: config.write(f) + def sync(self): + """Sync all local media files in the media folder to the remote instance""" + config = Config() + if config.config.has_section("media") is False: + config.config.add_section("media") + + media_path = config.project_path / "media" + if not media_path.is_dir(): + click.secho(f"Could not locate media folder '{media_path}'", fg="red") + return 1 + + api = API() + + r = api.get("/api/v1/files?type=page") + r.raise_for_status() + remote_files = {f"/files/{f['location']}": f for f in r.json()["data"]} + + for local_file in sorted(media_path.rglob("*")): + if not local_file.is_file() or local_file.name.startswith("."): + continue + + location = f"media/{local_file.relative_to(media_path).as_posix()}" + remote_file = remote_files.get(config["media"].get(location)) + + if remote_file: + with open(local_file, mode="rb") as file_handle: + local_sha1sum = hash_file(file_handle) + + if remote_file.get("sha1sum") == local_sha1sum: + click.secho(f"Skipping unchanged media '{location}'", fg="yellow") + continue + + r = api.delete(f"/api/v1/files/{remote_file['id']}") + r.raise_for_status() + + server_location = Media.upload(api, local_file, location) + config.config.set("media", location, f"/files/{server_location}") + click.secho(f"Synced media '{location}'", fg="green") + + with open(config.config_path, "w+") as f: + config.write(f) + + return 0 + def rm(self, path): """Remove local media file from remote server and local config""" config = Config() diff --git a/ctfcli/core/media.py b/ctfcli/core/media.py index cf77150..0a745b2 100644 --- a/ctfcli/core/media.py +++ b/ctfcli/core/media.py @@ -1,9 +1,25 @@ +import os + from ctfcli.core.config import Config from ctfcli.core.exceptions import ProjectNotInitialized from ctfcli.utils.tools import safe_format class Media: + @staticmethod + def upload(api, path, location: str) -> str: + file_payload = { + "type": "page", + "location": location, + } + + with open(path, mode="rb") as file_handle: + # Specifically use data= here to send multipart/form-data + r = api.post("/api/v1/files", files={"file": (os.path.basename(path), file_handle)}, data=file_payload) + r.raise_for_status() + resp = r.json() + return resp["data"][0]["location"] + @staticmethod def replace_placeholders(content: str) -> str: config = Config()