Skip to content

Analyze file version storage across a SharePoint document library.

Reports the number of versions per file, their storage cost, and identifies candidates for version cleanup (files with excessive versions that consume disproportionate storage).

Inspired by Report-SPOFileVersions.PS1 from Office 365 for IT Pros.

Permissions

Required delegated permissions: Sites.Read.All Read files, libraries, and version history

Reference

View source

import argparse
from typing import List

from office365.sharepoint.client_context import ClientContext
from tests.settings import client_id, password, site_url, tenant, username

_KB = 1024
_VERSION_THRESHOLD = 20


def format_bytes(size: int) -> str:
    """Format byte count to human-readable string."""
    if size >= _KB**3:
        return f"{size / (_KB**3):.2f} GB"
    elif size >= _KB**2:
        return f"{size / (_KB**2):.2f} MB"
    elif size >= _KB:
        return f"{size / _KB:.2f} KB"
    return f"{size} B"


def analyze_version_storage(ctx: ClientContext, library_name: str = "Shared Documents") -> List[dict]:
    """Analyze file version storage in a document library.

    Args:
        ctx: Authenticated ClientContext.
        library_name: Name of the document library to scan.

    Returns:
        List of file dicts with version count, total version size, and current size.
    """
    results = []

    try:
        lib = ctx.web.lists.get_by_title(library_name)
        files = lib.root_folder.files.expand(["Versions"]).get().execute_query()
    except Exception as e:
        print(f"  Error accessing library '{library_name}': {e}")
        return results

    for f in files:
        version_count = 0
        total_version_size = 0
        current_size = getattr(f, "length", 0)

        try:
            versions = f.versions
            if versions:
                version_count = len(versions)
                for v in versions:
                    total_version_size += getattr(v, "size", 0) or 0
        except Exception:
            pass

        results.append(
            {
                "name": getattr(f, "name", "Unknown"),
                "url": getattr(f, "server_relative_url", ""),
                "current_size": current_size,
                "version_count": version_count,
                "total_version_size": total_version_size,
                "storage_ratio": (total_version_size / current_size) if current_size > 0 else 0,
            }
        )

    # Sort by version count descending
    results.sort(key=lambda r: r["version_count"], reverse=True)
    return results


def main():
    parser = argparse.ArgumentParser(description="Analyze file version storage in a library")
    parser.add_argument("--library", default="Shared Documents", help="document library to scan")
    parser.add_argument(
        "--threshold", type=int, default=_VERSION_THRESHOLD, help="version count that flags a cleanup candidate"
    )
    args = parser.parse_args()

    print("Analyzing file version storage...\n")

    ctx = ClientContext(site_url).with_username_and_password(
        tenant=tenant, client_id=client_id, username=username, password=password
    )

    report = analyze_version_storage(ctx, args.library)

    if not report:
        print("No files found or library inaccessible.")
        return

    total_versions = sum(r["version_count"] for r in report)
    total_storage = sum(r["total_version_size"] for r in report)

    print(f"Files analyzed:     {len(report)}")
    print(f"Total versions:     {total_versions}")
    print(f"Version storage:    {format_bytes(total_storage)}")
    print()

    # Top 10 files by version count
    print("=== Top 10 files by version count ===\n")
    print(f"{'File':50s} {'Versions':>10s} {'Version Size':>15s} {'Current Size':>15s} {'Ratio'}")
    print("-" * 100)
    for r in report[:10]:
        print(
            f"{r['name'][:48]:50s} "
            f"{r['version_count']:>10d} "
            f"{format_bytes(r['total_version_size']):>15s} "
            f"{format_bytes(r['current_size']):>15s} "
            f"{r['storage_ratio']:.1f}x"
        )

    # Files with excessive versions
    excessive = [r for r in report if r["version_count"] >= args.threshold]
    if excessive:
        print(f"\n=== Files with {args.threshold}+ versions (cleanup candidates) ===\n")
        for r in excessive:
            print(f"  {r['name']} — {r['version_count']} versions, {format_bytes(r['total_version_size'])}")


if __name__ == "__main__":
    main()

← Back to Files