/* eslint-disable no-console */
/**
* Storage entity class for GitHub repository storage operations
* @file storage.js
* @license Apache-2.0
* @version 3.0.0
*
* @author Michael Hay <michael.hay@mediumroast.io>
* @copyright 2025 Mediumroast, Inc. All rights reserved.
*/
import { BaseObjects } from '../baseObjects.js';
import { logger } from '../logger.js';
export class Storage extends BaseObjects {
/**
* @constructor
* @param {string} token - GitHub API token
* @param {string} org - GitHub organization name
* @param {string} processName - Process name for locking
*/
constructor(token, org, processName) {
super(token, org, processName, 'Storage');
// Add storage-specific cache keys
this._cacheKeys.storageBilling = 'storage_billing';
this._cacheKeys.byContainer = 'storage_by_container';
this._cacheKeys.quota = 'storage_quota';
this._cacheKeys.trends = 'storage_trends';
// Set specific cache timeouts
this.cacheTimeouts.storageBilling = 3600000; // 1 hour for billing info
this.cacheTimeouts.byContainer = 3600000; // 1 hour for container info
this.cacheTimeouts.quota = 86400000; // 24 hours for quota info
this.cacheTimeouts.trends = 86400000; // 24 hours for trends
// Define object file names for containers
this.objectFiles = {
Studies: 'studies.json',
Companies: 'companies.json',
Interactions: 'interactions.json'
};
}
/**
* Get repository size information
* @returns {Promise<Array>} Size information
*/
async getRepoSize() {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'getRepoSize') :
{ end: () => {} };
try {
return await this.cache.getOrFetch(
this._cacheKeys.repoSize,
async () => {
try {
// Try to use getRepository method first
if (typeof this.serverCtl.getRepository === 'function') {
const repoResponse = await this.serverCtl.getRepository();
if (!repoResponse[0]) {
return repoResponse;
}
// Extract just the size from the response
return this._createSuccess(
'Retrieved repository size successfully',
repoResponse[2].size || 0
);
}
// Try to use getRepoSize (older method name) as fallback
else if (typeof this.serverCtl.getRepoSize === 'function') {
logger.info('Using legacy getRepoSize method');
return await this.serverCtl.getRepoSize();
}
// If neither method exists, provide a fallback response
else {
logger.warn('Repository size methods not implemented in github.js, using fallback');
return this._createSuccess(
'Repository size functionality not fully implemented',
{
size: 0,
message: 'This is a placeholder. The getRepository method needs to be implemented in the github.js file.'
}
);
}
} catch (error) {
// Handle any unexpected errors
logger.error('Failed to retrieve repository size', error);
throw error; // Re-throw to be caught by outer try-catch
}
},
this.cacheTimeouts.repoSize || 3600000,
[] // No dependencies
);
} catch (error) {
return this._createError(
`Failed to retrieve repository size: ${error.message}`,
error,
500
);
} finally {
tracking.end();
}
}
/**
* Get storage billing information
* @returns {Promise<Array>} Storage billing info
*/
async getStorageBilling() {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'getStorageBilling') :
{ end: () => {} };
try {
return await this.cache.getOrFetch(
this._cacheKeys.storageBilling,
async () => {
try {
// Check if the method exists first
if (typeof this.serverCtl.getStorageBillings === 'function') {
return await this.serverCtl.getStorageBillings();
} else {
// Provide fallback mock data
logger.warn('getStorageBillings not implemented in github.js, using fallback');
return this._createSuccess(
'Storage billing functionality not fully implemented',
{
days_left_in_billing_cycle: 15,
estimated_paid_storage_for_month: 0,
estimated_storage_for_month: 5,
message: 'This is a placeholder. The getStorageBillings method needs to be implemented.'
}
);
}
} catch (error) {
logger.error('Failed to retrieve storage billing', error);
throw error;
}
},
this.cacheTimeouts.storageBilling,
[]
);
} catch (error) {
return this._createError(
`Failed to retrieve storage billing: ${error.message}`,
error,
500
);
} finally {
tracking.end();
}
}
/**
* Get storage usage by container
* @returns {Promise<Array>} Storage usage by container
*/
async getStorageByContainer() {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'getStorageByContainer') :
{ end: () => {} };
try {
return await this.cache.getOrFetch(
this._cacheKeys.byContainer,
async () => {
try {
// Get all container names
const containers = ['Studies', 'Companies', 'Interactions'];
const stats = {
totalSize: 0,
containers: {}
};
for (const container of containers) {
// Skip if no object file for this container
if (!this.objectFiles[container]) {
logger.debug(`Skipping container ${container} - no object file defined`);
continue;
}
// Initialize container statistics
stats.containers[container] = {
size: 0,
objectCount: 0,
lastUpdated: null
};
// Get container objects
const containerClass = new BaseObjects(
this.serverCtl.token,
this.serverCtl.orgName,
'storage-analyzer',
container
);
const objectsResp = await containerClass.getAll();
if (!objectsResp[0]) {
logger.warn(`Failed to get objects for ${container}: ${objectsResp[1]?.status_msg}`);
continue;
}
const objects = objectsResp[2].mrJson;
stats.containers[container].objectCount = objects.length;
// Get latest modification date
for (const obj of objects) {
if (obj.modification_date &&
(!stats.containers[container].lastUpdated ||
new Date(obj.modification_date) > new Date(stats.containers[container].lastUpdated))) {
stats.containers[container].lastUpdated = obj.modification_date;
}
}
// For Interactions, also calculate total file size
if (container === 'Interactions') {
let totalInteractionSize = 0;
for (const obj of objects) {
if (obj.file_size) {
totalInteractionSize += obj.file_size;
}
}
stats.containers[container].fileSize = totalInteractionSize;
}
// Get container file size from SHA
try {
if (typeof this.serverCtl.getSha === 'function') {
const shaResp = await this.serverCtl.getSha(
container,
this.objectFiles[container],
'main'
);
if (shaResp[0] && shaResp[2] && typeof this.serverCtl.getContent === 'function') {
const contentResp = await this.serverCtl.getContent(
`${container}/${this.objectFiles[container]}`,
'main'
);
if (contentResp[0] && contentResp[2] && contentResp[2].size) {
stats.containers[container].size = contentResp[2].size;
stats.totalSize += contentResp[2].size;
}
}
}
} catch (err) {
logger.error(`Error getting size for ${container}:`, err);
}
}
return this._createSuccess(
'Retrieved storage usage by container',
stats
);
} catch (err) {
return this._createError(
`Failed to retrieve storage usage: ${err.message}`,
err,
500
);
}
},
this.cacheTimeouts.byContainer,
[]
);
} finally {
tracking.end();
}
}
/**
* Get storage usage trends over time
* @param {number} days - Number of days to analyze
* @returns {Promise<Array>} Storage usage trends
*/
async getStorageTrends(days = 30) {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'getStorageTrends') :
{ end: () => {} };
// Validate parameters
const validationError = this._validateParams(
{ days },
{ days: 'number' }
);
if (validationError) return validationError;
try {
// Use cache with key that includes days parameter
const trendsCacheKey = `${this._cacheKeys.trends}_${days}`;
return await this.cache.getOrFetch(
trendsCacheKey,
async () => {
try {
const commitHistory = await this.serverCtl.getCommitHistory(days);
if (!commitHistory[0]) {
return commitHistory;
}
// Extract size information from commits
const sizeByDate = {};
const commits = commitHistory[2];
for (const commit of commits) {
const date = commit.commit.author.date.substring(0, 10); // YYYY-MM-DD
// Get the repo size at this commit
try {
const sizeResp = await this.serverCtl.getRepoSizeAtCommit(commit.sha);
if (sizeResp[0] && sizeResp[2]) {
sizeByDate[date] = sizeResp[2];
}
} catch (err) {
logger.error(`Error getting size at commit ${commit.sha}:`, err);
}
}
// Convert to array and sort by date
const trends = Object.entries(sizeByDate).map(([date, size]) => ({
date,
size
})).sort((a, b) => new Date(a.date) - new Date(b.date));
return this._createSuccess(
`Retrieved storage trends for the past ${days} days`,
trends
);
} catch (err) {
return this._createError(
`Failed to retrieve storage trends: ${err.message}`,
err,
500
);
}
},
this.cacheTimeouts.trends
);
} finally {
tracking.end();
}
}
/**
* Get storage quota and usage
* @returns {Promise<Array>} Storage quota and usage information
*/
async getQuota() {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'getQuota') :
{ end: () => {} };
try {
return await this.cache.getOrFetch(
this._cacheKeys.quota,
async () => {
try {
if (typeof this.serverCtl.getGitHubOrg === 'function') {
const orgResp = await this.serverCtl.getGitHubOrg();
if (!orgResp[0]) {
return orgResp;
}
// Provide basic quota information
const quota = {
organization: this.org,
plan: orgResp[2]?.plan || {
name: 'unknown',
space: 'unknown'
}
};
return this._createSuccess(
'Retrieved storage quota information',
quota
);
} else {
// Provide fallback mock data
return this._createSuccess(
'Storage quota functionality not fully implemented',
{
organization: this.org,
plan: {
name: 'team',
space: 'unlimited',
message: 'This is a placeholder. The getGitHubOrg method needs to be implemented.'
}
}
);
}
} catch (err) {
return this._createError(
`Failed to retrieve storage quota: ${err.message}`,
err,
500
);
}
},
this.cacheTimeouts.quota,
[]
);
} finally {
tracking.end();
}
}
/**
* Get disk usage analytics
* @returns {Promise<Array>} Disk usage analytics
*/
async getDiskUsageAnalytics() {
// Track this operation
const tracking = logger.trackOperation ?
logger.trackOperation(this.objType, 'getDiskUsageAnalytics') :
{ end: () => {} };
try {
// Use cache with dependencies on container and repo size
const analyticsCacheKey = 'storage_analytics';
return await this.cache.getOrFetch(
analyticsCacheKey,
async () => {
try {
// Get storage by container
const containerResp = await this.getStorageByContainer();
if (!containerResp[0]) {
return containerResp;
}
const storage = containerResp[2];
// Calculate analytics
const analytics = {
totalSize: storage.totalSize,
repoSize: storage.repoSize || 0,
containers: {},
percentages: {},
largestContainer: {
name: '',
size: 0
},
mostObjects: {
name: '',
count: 0
},
mostRecentActivity: {
name: '',
date: null
}
};
// Calculate container analytics
for (const [name, container] of Object.entries(storage.containers)) {
// Copy container data
analytics.containers[name] = container;
// Calculate percentage of total
analytics.percentages[name] = storage.totalSize > 0 ?
(container.size / storage.totalSize) * 100 : 0;
// Track largest container
if (container.size > analytics.largestContainer.size) {
analytics.largestContainer = {
name,
size: container.size
};
}
// Track container with most objects
if (container.objectCount > analytics.mostObjects.count) {
analytics.mostObjects = {
name,
count: container.objectCount
};
}
// Track most recent activity
if (container.lastUpdated &&
(!analytics.mostRecentActivity.date ||
new Date(container.lastUpdated) > new Date(analytics.mostRecentActivity.date))) {
analytics.mostRecentActivity = {
name,
date: container.lastUpdated
};
}
}
// Add projected growth based on trends
try {
const trendsResp = await this.getStorageTrends(30);
if (trendsResp[0] && trendsResp[2] && trendsResp[2].length > 1) {
const trends = trendsResp[2];
const firstSize = trends[0].size;
const lastSize = trends[trends.length - 1].size;
const growthRate = (lastSize - firstSize) / firstSize;
analytics.growth = {
rate: growthRate,
period: '30 days',
projectedSize: {
'30days': Math.round(lastSize * (1 + growthRate)),
'90days': Math.round(lastSize * Math.pow(1 + growthRate, 3))
}
};
}
} catch (err) {
logger.warn('Failed to calculate growth projections', err);
}
return this._createSuccess(
'Generated storage analytics',
analytics
);
} catch (err) {
return this._createError(
`Failed to generate disk usage analytics: ${err.message}`,
err,
500
);
}
},
300000, // 5 minutes cache
[
this._cacheKeys.byContainer, // Depends on container data
this._cacheKeys.trends, // Depends on trends data
this._cacheKeys.repoSize // Depends on repo size
]
);
} finally {
tracking.end();
}
}
}