Source: gitHubServer/entities/storage.js

/* 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();
    }
  }
}