If you’re running JFrog Artifactory OSS, you’ll quickly notice that built-in cleanup features like Cleanup Policies are only available with an Enterprise+ license.
In this guide, I’ll show you how to:
- Delete artifacts older than N days
- Recursively clean up empty folders
- Do it all using only Python and the REST API
Why Not Use Cleanup Policies, User Plugins, or Workers?
- Cleanup Policies require Enterprise+
- User Plugins are not executable in OSS (they return HTTP 403/500)
- JFrog Workers are also not available in OSS
- This guide uses only public REST APIs, which work in any version, including OSS
- Delete Old Artifacts via AQL
Create a script called cleanup_old_artifacts.py
:
#!/usr/bin/env python3
import os
import sys
import requests
from requests.auth import HTTPBasicAuth
# === CONFIGURATION ===
ARTIFACTORY_URL = "https://artifactory.maksonlee.com/artifactory"
REPO_NAME = "product-snapshots"
USERNAME = "maksonlee"
PASSWORD = os.environ.get("ARTIFACTORY_PASSWORD")
DAYS = 30
AUTH = HTTPBasicAuth(USERNAME, PASSWORD)
def query_old_artifacts(repo: str, days: int):
print(f"[INFO] Querying artifacts older than {days} days in repo '{repo}'...")
aql_query = f'''
items.find({{
"repo": "{repo}",
"type": "file",
"created": {{ "$before": "{days}d" }}
}}).include("repo", "path", "name")
'''.strip()
response = requests.post(
f"{ARTIFACTORY_URL}/api/search/aql",
data=aql_query,
headers={"Content-Type": "text/plain"},
auth=AUTH
)
if response.status_code != 200:
print(f"[ERROR] AQL query failed: {response.status_code}")
print(response.text)
sys.exit(1)
return response.json().get("results", [])
def delete_artifact(repo: str, path: str, name: str):
url = f"{ARTIFACTORY_URL}/{repo}/{path}/{name}"
print(f"[DELETE] {url}")
response = requests.delete(url, auth=AUTH)
if response.status_code not in (200, 204):
print(f"[WARN] Failed to delete {url}: {response.status_code} {response.text}")
# === EXECUTION ===
artifacts = query_old_artifacts(REPO_NAME, DAYS)
print(f"[INFO] Found {len(artifacts)} files to delete.")
for item in artifacts:
delete_artifact(item["repo"], item["path"], item["name"])
print("[DONE] Cleanup complete.")
- Recursively Delete Empty Folders
Create another script called cleanup_empty_folders.py
:
#!/usr/bin/env python3
import os
import requests
from requests.auth import HTTPBasicAuth
# === CONFIGURATION ===
ARTIFACTORY_URL = "https://artifactory.maksonlee.com/artifactory"
REPO_NAME = "product-snapshots"
USERNAME = "maksonlee"
PASSWORD = os.environ.get("ARTIFACTORY_PASSWORD")
AUTH = HTTPBasicAuth(USERNAME, PASSWORD)
deleted_count = 0
def get_storage_url(path: str) -> str:
return f"{ARTIFACTORY_URL}/api/storage/{REPO_NAME}/{path}" if path else f"{ARTIFACTORY_URL}/api/storage/{REPO_NAME}"
def is_folder_empty(path: str) -> bool:
url = get_storage_url(path)
resp = requests.get(url, auth=AUTH)
if resp.status_code != 200:
print(f"[WARN] Failed to check folder: {url}")
return False
info = resp.json()
return not info.get("children", [])
def delete_folder(path: str) -> bool:
global deleted_count
if path == "":
print(f"[SKIP] Will not delete repository root: {REPO_NAME}")
return False
url = f"{ARTIFACTORY_URL}/{REPO_NAME}/{path}"
print(f"[DELETE] {url}")
resp = requests.delete(url, auth=AUTH)
if resp.status_code in (200, 204):
deleted_count += 1
return True
else:
print(f"[WARN] Failed to delete {url}: {resp.status_code} {resp.text}")
return False
def clean_folder(path: str) -> bool:
"""
Recursively delete empty folders, and return True if this folder was deleted.
"""
url = get_storage_url(path)
resp = requests.get(url, auth=AUTH)
if resp.status_code != 200:
print(f"[WARN] Failed to access folder: {url}")
return False
info = resp.json()
children = info.get("children", [])
for child in children:
name = child["uri"].lstrip("/")
full_child_path = f"{path}/{name}" if path else name
if child["folder"]:
clean_folder(full_child_path)
# Explicitly prevent deletion of repo root
if path and is_folder_empty(path):
return delete_folder(path)
return False
# === EXECUTION ===
print(f"[INFO] Starting recursive cleanup in repository '{REPO_NAME}'...")
clean_folder("") # Start from root of the repo
print(f"[DONE] Deleted {deleted_count} empty folders.")
- Running the Scripts
export ARTIFACTORY_PASSWORD='your-artifactory-password'
python3 cleanup_old_artifacts.py
python3 cleanup_empty_folders.py