Skip to content

Commit 404e02a

Browse files
authored
Make storage directory configurable (#389)
1 parent 3e40d97 commit 404e02a

File tree

11 files changed

+130
-17
lines changed

11 files changed

+130
-17
lines changed

eslint.config.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,12 @@ export default tseslint.config([
182182
message: 'Use methods in File.ts',
183183
},
184184
],
185+
patterns: [
186+
{
187+
group: ['**/ArtifactsDir'],
188+
message: 'Use Storage.ts instead. ArtifactsDir is deprecated.',
189+
},
190+
],
185191
},
186192
],
187193
},

src/app/standalone.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import { LoggerFactory } from '../telemetry/LoggerFactory';
88
import { TelemetryService } from '../telemetry/TelemetryService';
99
import { AwsEnv, NodeEnv, ProcessPlatform } from '../utils/Environment';
1010
import { ExtensionId, ExtensionName, ExtensionVersion } from '../utils/ExtensionConfig';
11+
import { Storage } from '../utils/Storage';
1112

1213
let server: unknown;
1314

1415
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, no-console */
1516
async function onInitialize(params: ExtendedInitializeParams) {
1617
const ClientInfo = params.clientInfo;
1718
const AwsMetadata = params.initializationOptions?.['aws'];
19+
Storage.initialize(AwsMetadata?.storageDir);
1820
LoggerFactory.initialize(AwsMetadata?.logLevel);
1921

2022
getLogger().info(

src/datastore/DataStore.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { pathToArtifact } from '../utils/ArtifactsDir';
21
import { Closeable } from '../utils/Closeable';
32
import { isWindows } from '../utils/Environment';
3+
import { pathToStorage } from '../utils/Storage';
44
import { FileStoreFactory } from './FileStore';
55
import { LMDBStoreFactory } from './LMDB';
66
import { MemoryStoreFactory } from './MemoryStore';
@@ -62,11 +62,11 @@ export class MultiDataStoreFactoryProvider implements DataStoreFactoryProvider {
6262
private readonly memoryStoreFactory: MemoryStoreFactory;
6363
private readonly persistedStore: DataStoreFactory;
6464

65-
constructor(rootDir: string = pathToArtifact()) {
65+
constructor() {
6666
if (isWindows) {
67-
this.persistedStore = new FileStoreFactory(rootDir);
67+
this.persistedStore = new FileStoreFactory(pathToStorage());
6868
} else {
69-
this.persistedStore = new LMDBStoreFactory(rootDir);
69+
this.persistedStore = new LMDBStoreFactory(pathToStorage());
7070
}
7171

7272
this.memoryStoreFactory = new MemoryStoreFactory();

src/server/InitParams.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type AwsMetadata = {
1414
};
1515
telemetryEnabled?: boolean;
1616
logLevel?: LevelWithSilent;
17+
storageDir?: string;
1718
cloudformation?: {
1819
endpoint?: string;
1920
};

src/telemetry/LoggerFactory.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { readdir, stat, unlink, writeFile, readFile } from 'fs/promises'; // esl
44
import { join } from 'path';
55
import { DateTime } from 'luxon';
66
import pino, { LevelWithSilent, Logger } from 'pino';
7-
import { pathToArtifact } from '../utils/ArtifactsDir';
87
import { Closeable } from '../utils/Closeable';
98
import { ExtensionId, ExtensionName } from '../utils/ExtensionConfig';
9+
import { pathToStorage } from '../utils/Storage';
1010
import { TelemetrySettings } from './TelemetryConfig';
1111

1212
export class LoggerFactory implements Closeable {
@@ -137,16 +137,16 @@ export class LoggerFactory implements Closeable {
137137

138138
static getLogger(clazz: string | Function): Logger {
139139
if (LoggerFactory._instance === undefined) {
140-
LoggerFactory.initialize();
140+
throw new Error('Logger not configured');
141141
}
142142
return LoggerFactory._instance.getLogger(clazz);
143143
}
144144

145-
static initialize(logLevel?: LevelWithSilent, rootDir: string = pathToArtifact()) {
145+
static initialize(logLevel?: LevelWithSilent) {
146146
if (LoggerFactory._instance !== undefined) {
147147
throw new Error('Logger was already configured');
148148
}
149-
LoggerFactory._instance = new LoggerFactory(rootDir, logLevel ?? TelemetrySettings.logLevel);
149+
LoggerFactory._instance = new LoggerFactory(pathToStorage(), logLevel ?? TelemetrySettings.logLevel);
150150
}
151151

152152
static reconfigure(newLevel: LevelWithSilent) {

src/utils/ArtifactsDir.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@ function getOrCreateAbsolutePath(artifactDir: string | undefined = undefined): s
2929
* Specify a subdirectory of the root artifacts directory, or undefined to create artifacts in root
3030
* @param artifactDir
3131
*/
32-
export function pathToArtifact(artifactDir: string | undefined = undefined): string {
32+
export function legacyPathToArtifact(artifactDir: string | undefined = undefined): string {
3333
return getOrCreateAbsolutePath(artifactDir);
3434
}

src/utils/Storage.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { existsSync, mkdirSync, rmSync } from 'fs';
2+
import { homedir, platform } from 'os';
3+
import { join } from 'path';
4+
import { legacyPathToArtifact } from './ArtifactsDir'; // eslint-disable-line no-restricted-imports
5+
import { ExtensionId } from './ExtensionConfig';
6+
7+
const AppName = ExtensionId;
8+
9+
/**
10+
* Manages the storage directory for language server artifacts (logs, caches, databases).
11+
* @example
12+
* // Initialize with default location
13+
* Storage.initialize();
14+
*
15+
* // Initialize with custom location
16+
* Storage.initialize('/custom/path');
17+
*
18+
* // Get path to storage root
19+
* const root = Storage.pathToStorage();
20+
*
21+
* // Get path to subdirectory (creates if needed)
22+
* const logsDir = Storage.pathToStorage('logs');
23+
*/
24+
export class Storage {
25+
private static root: string | undefined;
26+
27+
/**
28+
* Initialize storage with optional custom directory. Must be called before pathToStorage().
29+
* Cleans up legacy storage location (.aws-cfn-storage) from previous versions.
30+
* @param storageDir Optional explicit storage directory path
31+
*/
32+
static initialize(storageDir?: string): void {
33+
this.root = storageDir ?? this.getDefaultStorageRoot();
34+
this.cleanupLegacyStorage();
35+
}
36+
37+
/**
38+
* Get absolute path to storage directory or subdirectory. Creates directory if it doesn't exist.
39+
* @param artifactDir Optional subdirectory name
40+
* @returns Absolute path to the storage location
41+
* @throws Error if initialize() has not been called
42+
*/
43+
static pathToStorage(artifactDir?: string): string {
44+
if (this.root === undefined) {
45+
throw new Error('Storage directory not initialized');
46+
}
47+
const path = artifactDir ? join(this.root, artifactDir) : this.root;
48+
49+
if (!existsSync(path)) {
50+
mkdirSync(path, { recursive: true });
51+
}
52+
53+
return path;
54+
}
55+
56+
/**
57+
* Storage location priority:
58+
* 1. Explicit path passed to initialize()
59+
* 2. CFN_LSP_STORAGE_DIR environment variable
60+
* 3. Platform-specific default:
61+
* - Windows: %LOCALAPPDATA%\aws-cloudformation-languageserver
62+
* - macOS: ~/Library/Application Support/aws-cloudformation-languageserver
63+
* - Linux: $XDG_STATE_HOME/aws-cloudformation-languageserver (or ~/.local/state/...)
64+
*/
65+
private static getDefaultStorageRoot(): string {
66+
if (process.env.CFN_LSP_STORAGE_DIR) {
67+
return process.env.CFN_LSP_STORAGE_DIR;
68+
}
69+
70+
switch (platform()) {
71+
case 'win32': {
72+
return join(process.env.LOCALAPPDATA ?? join(homedir(), 'AppData', 'Local'), AppName);
73+
}
74+
case 'darwin': {
75+
return join(homedir(), 'Library', 'Application Support', AppName);
76+
}
77+
default: {
78+
return join(process.env.XDG_STATE_HOME ?? join(homedir(), '.local', 'state'), AppName);
79+
}
80+
}
81+
}
82+
83+
private static cleanupLegacyStorage(): void {
84+
const legacyPath = legacyPathToArtifact();
85+
if (existsSync(legacyPath)) {
86+
rmSync(legacyPath, { recursive: true, force: true });
87+
}
88+
}
89+
}
90+
91+
/**
92+
* Convenience function for Storage.pathToStorage()
93+
* @param artifactDir Optional subdirectory name
94+
*/
95+
export function pathToStorage(artifactDir?: string): string {
96+
return Storage.pathToStorage(artifactDir);
97+
}

tools/telemetry-generator.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import { readdirSync } from 'fs';
44
import { join, extname, resolve } from 'path';
55
import yargs from 'yargs';
66
import { hideBin } from 'yargs/helpers';
7+
import { Storage } from '../src/utils/Storage';
8+
import { ExtendedInitializeParams } from '../src/server/InitParams';
79
import { LoggerFactory } from '../src/telemetry/LoggerFactory';
810
import { TelemetryService } from '../src/telemetry/TelemetryService';
911

1012
const id = v4();
13+
const rootDir = join(process.cwd(), 'node_modules', '.cache', 'telemetry-generator', id);
1114
const initParams: ExtendedInitializeParams = {
1215
capabilities: {},
1316
processId: 1,
@@ -23,12 +26,13 @@ const initParams: ExtendedInitializeParams = {
2326
clientId: id,
2427
},
2528
logLevel: 'warn',
29+
storageDir: rootDir,
2630
},
2731
},
2832
};
2933

30-
const rootDir = join(process.cwd(), 'node_modules', '.cache', 'telemetry-generator', id);
31-
LoggerFactory.initialize('warn', rootDir);
34+
Storage.initialize(rootDir);
35+
LoggerFactory.initialize('warn');
3236
TelemetryService.initialize(
3337
initParams?.initializationOptions?.aws?.clientInfo?.extension,
3438
initParams?.initializationOptions?.aws,
@@ -64,7 +68,6 @@ import { LspStackHandlers } from '../src/protocol/LspStackHandlers';
6468
import { LspResourceHandlers } from '../src/protocol/LspResourceHandlers';
6569
import { LspRelatedResourcesHandlers } from '../src/protocol/LspRelatedResourcesHandlers';
6670
import { LspS3Handlers } from '../src/protocol/LspS3Handlers';
67-
import { ExtendedInitializeParams } from '../src/server/InitParams';
6871
import { RelationshipSchemaService } from '../src/services/RelationshipSchemaService';
6972
import { LspCfnEnvironmentHandlers } from '../src/protocol/LspCfnEnvironmentHandlers';
7073
import { FeatureFlagProvider, getFromGitHub } from '../src/featureFlag/FeatureFlagProvider';
@@ -189,7 +192,7 @@ function main() {
189192
stubInterface<LspS3Handlers>(),
190193
);
191194

192-
const dataStoreFactory = new MultiDataStoreFactoryProvider(rootDir);
195+
const dataStoreFactory = new MultiDataStoreFactoryProvider();
193196
const core = new CfnInfraCore(lsp, initParams, {
194197
dataStoreFactory,
195198
documentManager: new DocumentManager(textDocuments),

tst/setup.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { join } from 'path';
22
import { v4 } from 'uuid';
33
import { LoggerFactory } from '../src/telemetry/LoggerFactory';
44
import { TelemetryService } from '../src/telemetry/TelemetryService';
5+
import { Storage } from '../src/utils/Storage';
56

6-
LoggerFactory.initialize('silent', join(process.cwd(), 'node_modules', '.cache', 'tests', v4()));
7+
Storage.initialize(join(process.cwd(), 'node_modules', '.cache', 'tests', v4()));
8+
LoggerFactory.initialize('silent');
79
TelemetryService.initialize(undefined, { telemetryEnabled: false });

tst/unit/datastore/FilestoreWorker.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { EncryptedFileStore } from '../../../src/datastore/file/EncryptedFileSto
44
import { encryptionKey } from '../../../src/datastore/file/Encryption';
55
import { LoggerFactory } from '../../../src/telemetry/LoggerFactory';
66
import { TelemetryService } from '../../../src/telemetry/TelemetryService';
7+
import { Storage } from '../../../src/utils/Storage';
78

89
// Worker script for multiprocess FileStore testing
9-
LoggerFactory.initialize('silent', join(process.cwd(), 'node_modules', '.cache', 'filedb-worker', v4()));
10+
Storage.initialize(join(process.cwd(), 'node_modules', '.cache', 'filedb-worker', v4()));
11+
LoggerFactory.initialize('silent');
1012
TelemetryService.initialize(undefined, { telemetryEnabled: false });
1113

1214
const [encTestDir, workerId, numWrites] = process.argv.slice(2);

0 commit comments

Comments
 (0)