Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
- [uninstall-pip](#uninstall-pip-command)
- [List](#list)
- [list-tools](#list-tools-command)
- [Cleanup](#cleanup)
- [cleanup-downloads](#cleanup-downloads-command)

---

Expand Down Expand Up @@ -518,3 +520,33 @@ When writing to a file, JSON output is minified.
```bash
containerbase-cli list tools --json --out /tmp/tools.json
```

<br>

## Cleanup

### `containerbase-cli cleanup downloads` <a name="cleanup-downloads-command"></a>

#### Description <a name="Description-cleanup-downloads"></a>

Evicts the oldest files from the download cache in `CONTAINERBASE_CACHE_DIR` down to `CONTAINERBASE_MAX_ALLOCATED_DISK`.

It's the same cleanup which runs after each install, see [download cache](../README.md#download-cache): the oldest files are removed while the disk holding the folder is filled to `CONTAINERBASE_MAX_ALLOCATED_DISK` percent or more.
Nothing is removed when `CONTAINERBASE_CACHE_DIR` isn't set.
`CONTAINERBASE_MAX_ALLOCATED_DISK` defaults to `100`, so only a full disk is cleaned, set a lower value, eg. `80`, to evict earlier, see [download cache](../README.md#download-cache).
With `--dry-run` only the oldest file is logged, as nothing is freed to check the fill level again.

#### Usage <a name="Usage-cleanup-downloads"></a>

`$ containerbase-cli cleanup downloads [-d,--dry-run]`

#### Examples <a name="Examples-cleanup-downloads"></a>

- Evicts the oldest downloads
```bash
containerbase-cli cleanup downloads
```
- Logs the oldest download which would be removed
```bash
containerbase-cli cleanup downloads --dry-run
```
31 changes: 31 additions & 0 deletions src/cli/cleanup/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, expect, test, vi } from 'vitest';
import { CacheService } from '../services/index.ts';
import { logger } from '../utils/index.ts';
import { cleanupDownloads } from './index.ts';

describe('cli/cleanup/index', () => {
describe('cleanupDownloads', () => {
test('evicts the download cache', async () => {
const spy = vi
.spyOn(CacheService.prototype, 'evict')
.mockResolvedValue(['/cache/a']);

expect(await cleanupDownloads(true)).toEqual(['/cache/a']);
expect(spy).toHaveBeenCalledExactlyOnceWith(true);
// the dry run file is logged by `evict` already
expect(logger.info).not.toHaveBeenCalled();

expect(await cleanupDownloads()).toEqual(['/cache/a']);
expect(logger.info).toHaveBeenCalledExactlyOnceWith(
'Removed 1 downloads',
);
});

test('logs when there is nothing to evict', async () => {
vi.spyOn(CacheService.prototype, 'evict').mockResolvedValue([]);

expect(await cleanupDownloads()).toEqual([]);
expect(logger.info).toHaveBeenCalledExactlyOnceWith('Nothing to evict');
});
});
});
21 changes: 21 additions & 0 deletions src/cli/cleanup/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { CacheService, createContainer } from '../services/index.ts';
import { logger } from '../utils/index.ts';

/**
* Evicts the oldest files from the download cache, see `CacheService.evict`,
* and logs how many were removed.
*
* @returns the removed files, with `dryRun` the file which would be removed
* first
*/
export async function cleanupDownloads(dryRun = false): Promise<string[]> {
const svc = await createContainer().getAsync(CacheService);
const removed = await svc.evict(dryRun);
if (!removed.length) {
logger.info('Nothing to evict');
} else if (!dryRun) {
// a dry run only returns the oldest file, which `evict` already logs
logger.info(`Removed ${removed.length} downloads`);
}
return removed;
}
50 changes: 50 additions & 0 deletions src/cli/command/cleanup-downloads.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, test, vi } from 'vitest';
import { logger } from '../utils/index.ts';
import { testCli } from '~test/di.ts';

const mocks = vi.hoisted(() => ({
cleanupDownloads: vi.fn(),
}));

vi.mock('../cleanup/index.ts', () => mocks);

describe('cli/command/cleanup-downloads', () => {
test('works', async () => {
const cli = testCli('containerbase-cli');
mocks.cleanupDownloads.mockResolvedValueOnce(['/cache/a', '/cache/b']);

expect(await cli.run(['cleanup', 'downloads'])).toBe(0);

expect(mocks.cleanupDownloads).toHaveBeenCalledExactlyOnceWith(false);
expect(logger.info).toHaveBeenCalledWith(
expect.stringContaining('Cleanup downloads succeeded'),
);
});

test('dry run', async () => {
const cli = testCli('containerbase-cli');
mocks.cleanupDownloads.mockResolvedValueOnce(['/cache/a']);

expect(await cli.run(['cleanup', 'downloads', '-d'])).toBe(0);

expect(mocks.cleanupDownloads).toHaveBeenCalledExactlyOnceWith(true);
});

test('fails', async () => {
const cli = testCli('containerbase-cli');
mocks.cleanupDownloads.mockRejectedValueOnce(new Error('test'));

expect(await cli.run(['cleanup', 'downloads'])).toBe(1);

expect(logger.error).toHaveBeenCalledWith('test');
expect(logger.fatal).toHaveBeenCalledWith(
expect.stringContaining('Cleanup downloads failed'),
);

// a rejection which is not an `Error` has no message to report
mocks.cleanupDownloads.mockRejectedValueOnce('boom');
expect(await cli.run(['cleanup', 'downloads'])).toBe(1);
expect(logger.debug).toHaveBeenCalledWith('boom');
expect(logger.error).not.toHaveBeenCalledWith('boom');
});
});
56 changes: 56 additions & 0 deletions src/cli/command/cleanup-downloads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Command, Option } from 'clipanion';
import prettyMilliseconds from 'pretty-ms';
import { cleanupDownloads } from '../cleanup/index.ts';
import { logger } from '../utils/index.ts';
import { command } from './utils.ts';

@command('containerbase-cli')
export class CleanupDownloadsCommand extends Command {
static override paths = [['cleanup', 'downloads']];

static override usage = Command.Usage({
description:
'Evicts the oldest files from `CONTAINERBASE_CACHE_DIR` down to `CONTAINERBASE_MAX_ALLOCATED_DISK`.',
examples: [
['Evicts the oldest downloads', '$0 cleanup downloads'],
[
'Logs the oldest download which would be removed',
'$0 cleanup downloads --dry-run',
],
],
});

dryRun = Option.Boolean('-d,--dry-run', false);

/**
* Evicts the oldest files from the download cache, returning a non-zero exit
* code on failure.
*/
async execute(): Promise<number | void> {
const start = Date.now();
let error = false;
logger.info('Cleanup downloads ...');
try {
await cleanupDownloads(this.dryRun);
return 0;
} catch (err) {
error = true;
logger.debug(err);
if (err instanceof Error) {
logger.error(err.message);
}
return 1;
/* v8 ignore next -- coverage bug */
} finally {
if (error) {
logger.fatal(
`Cleanup downloads failed in ${prettyMilliseconds(Date.now() - start)}.`,
);
} else {
logger.info(
`Cleanup downloads succeeded in ${prettyMilliseconds(Date.now() - start)}.`,
);
}
}
}
}
1 change: 1 addition & 0 deletions src/cli/command/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import './cleanup-downloads.ts';
import './cleanup-path.ts';
import './file-download.ts';
import './file-exists.ts';
Expand Down
Loading