Remove Old Artifacts and Empty Directories in Artifactory OSS

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

  1. 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.")

  1. 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.")

  1. Running the Scripts
export ARTIFACTORY_PASSWORD='your-artifactory-password'
python3 cleanup_old_artifacts.py
python3 cleanup_empty_folders.py

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top