Finding unused files

Our ‘Files’ are full of unused files. There doesn’t seem to be a way in HS to sort by usage or a filter for ‘unused’. I was hoping I could write a script using the API to get a list of the unused files and delete them.

However, the V3 API doesn’t seem to offer any meta-data for files. The V2 API does offer a ‘get file metadata’ call, but it doesn’t seem to offer anything that the V3 ‘get file’ call does. I am guessing that this is just a renaming of the endpoint.

So is there any way I can programmatically get information about the usage of files, similar to how I can use the HS file manager on the web? I really don’t want to have to sort through 1000’s of files, and one at a time clicking on details, delete, ok, rinse repeat.

Note that we have HS Marketing Pro, but we do not have/use the HS CMS platform. These are files used in our LP’s, social media posts, or LP templates.

I was able write a tampermonkey script that scraped the information about all the files (including usage) into a json object so that it can be used with a script which deletes the unused files. I’ll post it here hoping that it helps someone else.

  1. Install the chrome TamperMonkey plugin.
  2. Add this script below to TM.
  3. Go to the first page of your files.
  4. You should see a 'Get File Info button. Click the button.
  5. Don’t touch anything; wait until a popup shows the JSON, and copy paste it to where you need it.
// ==UserScript==
// @name Hubspot find unused
// @namespace http://tampermonkey.net/
// @version 0.1
// @description crawl the HS file explorer and make a printable list if they are not used.
// @author Shea Martin
// @match https://app.hubspot.com/files/*
// @icon https://www.hubspot.com/hubfs/assets/hubspot.com/style-guide/brand-guidelines/guidelines_approved-sprocket-2.svg
// @grant none
// @require http://code.jquery.com/jquery-latest.js
// ==/UserScript==
var $ = window.jQuery;

const maxPages = 20;

function getFileInfoFromSidePanel(r) {
 $('[data-key="FileManagerDashboard.detailPanel.fileUsage"]').click();
 $('[data-key="FileManagerDashboard.detailPanel.sections.general"]').click();

 const name = $('[data-test-id="panel-file-name"] > span').text();
 const url = $('[data-test-id=file-detail-url-link]').attr('href');
 const unused = $('[data-key="FileManagerDashboard.fileUsage.unused"]').length === 1;
 const size = $('[data-test-id=file-detail-size]').text();
 const dimensions = $('[data-key="FileManagerCore.fileDimensions.dimensions"]').text();
 const fileId = $('[data-test-id="panel-file-id"]').text();
 const fileType = $('[data-key="FileManagerDashboard.detailPanel.fileType"]').parent().next().text();

 return {
 name,
 url,
 unused,
 size,
 dimensions,
 fileId,
 fileType,
 };
}

function processRowsOnPage(rows, fileDetails, pageCompleteCallback) {
 const r = rows.pop();
 if(!r) {
 log(`completed page ${curPage}`);
 pageCompleteCallback();
 return;
 }
 r.click();
 setTimeout(() => {
 const details = getFileInfoFromSidePanel();
 fileDetails.push(details);
 log(`got details for ${details.name}`);
 processRowsOnPage(rows, fileDetails, pageCompleteCallback);
 }, 1000);
}

function processPage(fileDetails, allPagesCompleteCallback) {
 log(`processing page ${curPage}`);
 const rows = $('button[data-test-id=file-row]').toArray();
 processRowsOnPage(rows, fileDetails, () => {
 const hasNext = nextPage();
 if(hasNext) {
 processPage(fileDetails, allPagesCompleteCallback);
 } else {
 allPagesCompleteCallback();
 }
 });
}

function getUnused() {
 curPage = 1;
 const fileDetails = [];
 processPage(fileDetails, () => {
 log('complete');
 completeAllPages(fileDetails);
 });
}

function completeAllPages(fileDetails) {
 const content = JSON.stringify(fileDetails, undefined, 4);

 alert(`got information on ${fileDetails.length} files. Copy the results from the results pane.`);
 $('#file-results-pane').show();
 $('#file-results-data').html(content);
}

let curPage = 1;
function nextPage() {
 const nextButton = $('button[data-next-page=true]');
 if(curPage >= maxPages) {
 log(`page limit of ${maxPages} has been exceeded`);
 return false;
 }
 if(nextButton.length !== 1 || nextButton.attr('aria-disabled') === 'true') {
 log('no more pages');
 return false;
 }
 curPage += 1;
 nextButton.click();
 return true;
}

function setupUi() {
 log('ui initiated');
 const startButton = $('<button>Get File Info</button>');
 startButton.click(() => {
 alert('starting to retrieve file information. Do not touch mouse or keyboard until complete dialog shows.');
 getUnused();
 });
 $('[data-test-id=upload-files-button]').parent().append(startButton);

 const css = `
 border: 4px solid green;
 position: absolute;
 top: 100px;
 left: 20px;
 right: 20px;
 height: 80vh;
 padding: 40px;
 overflow: auto;
 z-index: 1000;
 background-color: white;
 font-family: monospace;
 display: none;
 `;

 $('body').prepend(`
 <div id="file-results-pane" style="${css}">
 <button onclick="$('#file-results-pane').hide();">Close</button>
 <h1>Results</h1>
 <pre id="file-results-data"></pre>
 </div>`);
}

function log(msg) {
 console.log('[tm] ' + msg);
}

(function() {
 'use strict';

 log('loaded');

 setTimeout(() => setupUi(), 3000);
 // setTimeout(() => completeAllPages([{name: 'shea', age: 43}, { name: 'Denby', age: 42 }]), 4000);

 // document.addEventListener('keydown', (e) => {
 // if(e.ctrlKey && e.key === 'D') {
 // log('shortcut pressed');
 // getUnused();
 // }
 // }, true);
})();

Thanks Sheamus (and no thanks to HubSpot)

the code worked well but I made a few augmentations to it.
Now it highlights and selects those files that are unused.
I kept the json return but left it hidden in case someone really wanted it

// ==UserScript==
// HubSpot -- Select Unused Files
// @namespace http://tampermonkey.net/
// @version 0.2
// @description make HubSpot life easier
// @author erik bloomstrand
// @match https://app.hubspot.com/files/*
// https://www.google.com/s2/favicons?sz=64&domain=hubspot.com
// none
// @require http://code.jquery.com/jquery-latest.js
// ==/UserScript==
var $ = window.jQuery;

const maxPages = 1;

function getFileInfoFromSidePanel(r) {
 $('[data-key="FileManagerDashboard.detailPanel.fileUsage"]').click();
 $('[data-key="FileManagerDashboard.detailPanel.sections.general"]').click();

 const name = $('[data-test-id="panel-file-name"] > span').text();
 const url = $('[data-test-id=file-detail-url-link]').attr('href');
 const unused = $('[data-key="FileManagerDashboard.fileUsage.unused"]').length === 1;
 const size = $('[data-test-id=file-detail-size]').text();
 const dimensions = $('[data-key="FileManagerCore.fileDimensions.dimensions"]').text();
 const fileId = $('[data-test-id="panel-file-id"]').text();
 const fileType = $('[data-key="FileManagerDashboard.detailPanel.fileType"]').parent().next().text();

 return {
 name,
 url,
 unused,
 size,
 dimensions,
 fileId,
 fileType,
 };
}

function processRowsOnPage(rows, fileDetails, pageCompleteCallback) {
 const r = rows.pop();
 if(!r) {
 log(`completed page ${curPage}`);
 pageCompleteCallback();
 return;
 }
 r.click();
 setTimeout(() => {
 const details = getFileInfoFromSidePanel();
 fileDetails.push(details);
 log(`got details for ${details.name} ${details.fileId} ${details.unused}`);
 processRowsOnPage(rows, fileDetails, pageCompleteCallback);
 if ( details.unused === true) {
 $('tr[data-test-object-id="'+details.fileId+'"]').css('background-color', '#FF000055').find('label').click();
 } else {
 $('tr[data-test-object-id="'+details.fileId+'"]').css('background-color', '#00FF0055');
 }
 }, 1000);
}

function processPage(fileDetails, allPagesCompleteCallback) {
 log(`processing page ${curPage}`);
 const rows = $('button[data-test-id=file-row]').toArray();
 processRowsOnPage(rows, fileDetails, () => {
 const hasNext = nextPage();
 if(hasNext) {
 processPage(fileDetails, allPagesCompleteCallback);
 } else {
 allPagesCompleteCallback();
 }
 });
}

function getUnused() {
 curPage = 1;
 const fileDetails = [];
 processPage(fileDetails, () => {
 log('complete');
 completeAllPages(fileDetails);
 });
}

function completeAllPages(fileDetails) {
 const content = JSON.stringify(fileDetails, undefined, 4);
 const unusedFiles = fileDetails.filter(file => file.unused === true);
 $('[data-test-id="close-panel-button"]').click();

 setTimeout(() => {
 alert(`${fileDetails.length} files have been evaluated. ${unusedFiles.length} are selected and not being used`);
 }, 200);
// $('#file-results-pane').show();
 $('#file-results-data').html(content);
}

let curPage = 1;
function nextPage() {
 const nextButton = $('button[data-next-page=true]');
 if(curPage >= maxPages) {
 log(`page limit of ${maxPages} has been exceeded`);
 return false;
 }
 if(nextButton.length !== 1 || nextButton.attr('aria-disabled') === 'true') {
 log('no more pages');
 return false;
 }
 curPage += 1;
 nextButton.click();
 return true;
}

function setupUi() {
 log('ui initiated');
 const startButton = $('<button class="uiButton private-button private-button--sm private-button--non-link">Select Unused Files</button>');
 startButton.click(() => {
 alert('Starting to retrieve file information. Do not touch mouse or keyboard until complete dialog shows.');
 getUnused();
 });
 $('[data-test-id=upload-files-button]').parent().append(startButton);

 const css = `
 border: 4px solid green;
 position: absolute;
 top: 100px;
 left: 20px;
 right: 20px;
 height: 80vh;
 padding: 40px;
 overflow: auto;
 z-index: 1000;
 background-color: white;
 font-family: monospace;
 display: none;
 `;

 $('body').prepend(`
 <div id="file-results-pane" style="${css}">
 <button onclick="$('#file-results-pane').hide();">Close</button>
 <h1>Results</h1>
 <pre id="file-results-data"></pre>
 </div>`);
}

function log(msg) {
 console.log('[tm] ' + msg);
}

(function() {
 'use strict';
 setTimeout(() => setupUi(), 3000);
})();

here is an example of the output

Hi @schmelik super cool ! I actually be really interested to have the exhaustive code if it is possible please ? :slightly_smiling_face:

To be honest, I was under a deadline, so I left out comments
horrible :winking_face_with_tongue:
I’ll circle back to add the exhaustive code when I find the time. -- no promises