| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as fs from 'fs/promises'; |
| import * as os from 'os'; |
| import * as path from 'path'; |
| import * as process from 'process'; |
| |
| // Functions to read/write metric properties (e.g. UUID, opt-in/out status), which are stored in |
| // ~/.fuchsia/metrics/<property-name> |
| |
| /** |
| * Get the property with the given name. |
| * @param name the name of the property |
| * @returns the property value without possible leading or trailing newlines, or undefined if the |
| * property does not exist |
| */ |
| export async function get(name: string): Promise<string | undefined> { |
| try { |
| return (await fs.readFile(getMetricPropertyPath(name))).toString().trim(); |
| } catch { |
| return undefined; |
| } |
| } |
| |
| /** |
| * Similar to get(), but returns a boolean. |
| * @param name the name of the property |
| * @returns true if and only if get() would return "1" |
| */ |
| export async function getBoolean(name: string): Promise<boolean | undefined> { |
| const content = await get(name); |
| if (content === undefined) { |
| return undefined; |
| } |
| return content === '1'; |
| } |
| |
| /** |
| * Set the property with the given name to the given value. |
| * @param name the name of the property |
| * @param value the value to set for the property |
| */ |
| export async function set(name: string, value: string): Promise<void> { |
| try { |
| await fs.mkdir(getMetricDirectory(), { recursive: true, mode: 0o700 }); |
| await fs.writeFile(getMetricPropertyPath(name), value + '\n', { mode: 0o600 }); |
| } catch (err) { |
| if (err instanceof Error) { |
| console.log(`Warning: unable to set analytics property ${name}`); |
| } |
| } |
| } |
| |
| /** |
| * Similar to set but for boolean value |
| * @param name the name of the property |
| * @param value the boolean value to set for the property |
| */ |
| export async function setBoolean(name: string, value: boolean): Promise<void> { |
| await set(name, value ? '1' : '0'); |
| } |
| |
| /** |
| * Delete the property with the given name. |
| * @param name the name of the property |
| */ |
| export async function deleteProperty(name: string): Promise<void> { |
| try { |
| await fs.rm(getMetricPropertyPath(name), { force: true }); |
| } catch (err) { |
| if (err instanceof Error) { |
| console.log(`Warning: unable to delete analytics property ${name}`); |
| } |
| } |
| } |
| |
| /** |
| * Check the existence of the property with the given name. |
| * @param name the name of the property |
| * @returns true if the property exists |
| */ |
| export async function exists(name: string): Promise<boolean> { |
| try { |
| await fs.access(getMetricPropertyPath(name)); |
| return true; |
| } catch { |
| return false; |
| } |
| } |
| |
| /** |
| * See below: |
| * @returns the base directory to put 'Fuchsia/metrics' folder |
| */ |
| function getMetricBaseDirectory(): string { |
| if (os.platform() === 'darwin') { |
| return path.join(os.homedir(), 'Library', 'Application Support'); |
| } |
| |
| if (process.env.XDG_DATA_HOME !== undefined) { |
| return process.env.XDG_DATA_HOME; |
| } |
| |
| return path.join(os.homedir(), '.local', 'share'); |
| } |
| |
| /** |
| * See below: |
| * @returns the directory to store persistent analytics status |
| */ |
| export function getMetricDirectory(): string { |
| return path.join(getMetricBaseDirectory(), 'Fuchsia', 'metrics'); |
| } |
| |
| /** |
| * See below: |
| * @returns old directory to store persistent analytics status |
| */ |
| function getOldMetricDirectory(): string { |
| return path.join(os.homedir(), '.fuchsia', 'metrics'); |
| } |
| |
| /** |
| * Get the file path for storing a specific metric property. |
| * @param name the name of the property |
| * @returns the path of the storage file |
| */ |
| function getMetricPropertyPath(name: string): string { |
| return path.join(getMetricDirectory(), name); |
| } |
| |
| /** |
| * Migrate persistence storage location from $HOME to $XDG_DATA_HOME |
| */ |
| export async function migrateMetricDirectory(): Promise<void> { |
| const metricDirectory = getMetricDirectory(); |
| try { |
| await fs.access(metricDirectory); |
| // no need to migrate as the new folder already exists |
| return; |
| } catch { |
| // continue to the next try |
| } |
| |
| const oldMetricDirectory = getOldMetricDirectory(); |
| try { |
| await fs.access(oldMetricDirectory); |
| } catch { |
| // no need to migrate as the old folder does not exist |
| return; |
| } |
| |
| try { |
| await fs.mkdir(path.dirname(getMetricDirectory()), { recursive: true, mode: 0o700 }); |
| await fs.rename(oldMetricDirectory, metricDirectory); |
| // TODO(fxb/113028): remove symlinking and related tests when ready |
| await fs.symlink(metricDirectory, oldMetricDirectory, 'dir'); |
| } catch { |
| // We have tried our best and have to give up the migration. However, normal usage of |
| // analytics would not be affected. The only consequence is that migrated and non-migrated |
| // tools might have inconsistent analytics status. |
| } |
| } |