Skip to content

Commit 55516ef

Browse files
committed
feat: Added file utils
1 parent ff46f6d commit 55516ef

File tree

1 file changed

+216
-0
lines changed

1 file changed

+216
-0
lines changed

src/utils/file-utils.ts

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import * as fsSync from 'node:fs';
2+
import * as fs from 'node:fs/promises';
3+
import path from 'node:path';
4+
import { Readable } from 'node:stream';
5+
import { finished } from 'node:stream/promises';
6+
7+
import { Utils } from './index.js';
8+
9+
const SPACE_REGEX = /^\s*$/
10+
11+
export class FileUtils {
12+
static async downloadFile(url: string, destination: string): Promise<void> {
13+
console.log(`Downloading file from ${url} to ${destination}`);
14+
const { body } = await fetch(url)
15+
16+
const dirname = path.dirname(destination);
17+
if (!await fs.stat(dirname).then((s) => s.isDirectory()).catch(() => false)) {
18+
await fs.mkdir(dirname, { recursive: true });
19+
}
20+
21+
const ws = fsSync.createWriteStream(destination)
22+
// Different type definitions here for readable stream (NodeJS vs DOM). Small hack to fix that
23+
await finished(Readable.fromWeb(body as never).pipe(ws));
24+
25+
console.log(`Finished downloading to ${destination}`);
26+
}
27+
28+
static async addToStartupFile(line: string): Promise<void> {
29+
const lineToInsert = addLeadingSpacer(
30+
addTrailingSpacer(line)
31+
);
32+
33+
await fs.appendFile(Utils.getPrimaryShellRc(), lineToInsert)
34+
35+
function addLeadingSpacer(line: string): string {
36+
return line.startsWith('\n')
37+
? line
38+
: '\n' + line;
39+
}
40+
41+
function addTrailingSpacer(line: string): string {
42+
return line.endsWith('\n')
43+
? line
44+
: line + '\n';
45+
}
46+
}
47+
48+
static async addAllToStartupFile(lines: string[]): Promise<void> {
49+
const formattedLines = '\n' + lines.join('\n') + '\n';
50+
const shellRc = Utils.getPrimaryShellRc();
51+
52+
console.log(`Adding to ${path.basename(shellRc)}:
53+
${lines.join('\n')}`)
54+
55+
await fs.appendFile(shellRc, formattedLines)
56+
}
57+
58+
static async addPathToPrimaryShellRc(value: string, prepend: boolean): Promise<void> {
59+
const shellRc = Utils.getPrimaryShellRc();
60+
console.log(`Saving path: ${value} to ${shellRc}`);
61+
62+
if (prepend) {
63+
await fs.appendFile(shellRc, `\nexport PATH=$PATH:${value};`, { encoding: 'utf8' });
64+
return;
65+
}
66+
67+
await fs.appendFile(shellRc, `\nexport PATH=${value}:$PATH;`, { encoding: 'utf8' });
68+
}
69+
70+
static async dirExists(path: string): Promise<boolean> {
71+
let stat;
72+
try {
73+
stat = await fs.stat(path);
74+
return stat.isDirectory();
75+
} catch {
76+
return false;
77+
}
78+
}
79+
80+
static async fileExists(path: string): Promise<boolean> {
81+
let stat;
82+
try {
83+
stat = await fs.stat(path);
84+
return stat.isFile();
85+
} catch {
86+
return false;
87+
}
88+
}
89+
90+
static async exists(path: string): Promise<boolean> {
91+
try {
92+
await fs.stat(path);
93+
return true;
94+
} catch {
95+
return false;
96+
}
97+
}
98+
99+
static async checkDirExistsOrThrowIfFile(path: string): Promise<boolean> {
100+
let stat;
101+
try {
102+
stat = await fs.stat(path);
103+
} catch {
104+
return false;
105+
}
106+
107+
if (stat.isDirectory()) {
108+
return true;
109+
}
110+
111+
throw new Error(`Directory ${path} already exists and is a file`);
112+
}
113+
114+
static async createDirIfNotExists(path: string): Promise<void> {
115+
if (!fsSync.existsSync(path)) {
116+
await fs.mkdir(path, { recursive: true });
117+
}
118+
}
119+
120+
static async removeFromFile(filePath: string, search: string): Promise<void> {
121+
const contents = await fs.readFile(filePath, 'utf8');
122+
const newContents = contents.replaceAll(search, '');
123+
124+
await fs.writeFile(filePath, newContents, 'utf8');
125+
}
126+
127+
128+
static async removeLineFromFile(filePath: string, search: RegExp | string): Promise<void> {
129+
const file = await fs.readFile(filePath, 'utf8')
130+
const lines = file.split('\n');
131+
132+
let searchRegex;
133+
let searchString;
134+
135+
if (typeof search === 'object') {
136+
const startRegex = /^([\t ]*)?/;
137+
const endRegex = /([\t ]*)?/;
138+
139+
// Augment regex with spaces criteria to make sure this function is not deleting lines that are comments or has other content.
140+
searchRegex = search
141+
? new RegExp(
142+
startRegex.source + search.source + endRegex.source,
143+
search.flags
144+
)
145+
: search;
146+
}
147+
148+
if (typeof search === 'string') {
149+
searchString = search;
150+
}
151+
152+
for (let counter = lines.length; counter >= 0; counter--) {
153+
if (!lines[counter]) {
154+
continue;
155+
}
156+
157+
if (searchString && lines[counter].includes(searchString)) {
158+
lines.splice(counter, 1);
159+
continue;
160+
}
161+
162+
if (searchRegex && lines[counter].search(searchRegex) !== -1) {
163+
lines.splice(counter, 1);
164+
}
165+
}
166+
167+
await fs.writeFile(filePath, lines.join('\n'));
168+
console.log(`Removed line: ${search} from ${filePath}`)
169+
}
170+
171+
static async removeLineFromPrimaryShellRc(search: RegExp | string): Promise<void> {
172+
return FileUtils.removeLineFromFile(Utils.getPrimaryShellRc(), search);
173+
}
174+
175+
// Append the string to the end of a file ensuring at least 1 lines of space between.
176+
// Ex result:
177+
// something something;
178+
//
179+
// newline;
180+
static appendToFileWithSpacing(file: string, textToInsert: string): string {
181+
const lines = file.trimEnd().split(/\n/);
182+
if (lines.length === 0) {
183+
return textToInsert;
184+
}
185+
186+
const endingNewLines = FileUtils.calculateEndingNewLines(lines);
187+
const numNewLines = endingNewLines === -1
188+
? 0
189+
: Math.max(0, 2 - endingNewLines);
190+
return lines.join('\n') + '\n'.repeat(numNewLines) + textToInsert
191+
}
192+
193+
// This is overly complicated but it can be used to insert into any
194+
// position in the future
195+
private static calculateEndingNewLines(lines: string[]): number {
196+
let counter = 0;
197+
while (true) {
198+
const line = lines.at(-counter - 1);
199+
200+
if (!line) {
201+
return -1
202+
}
203+
204+
if (!SPACE_REGEX.test(line)) {
205+
return counter;
206+
}
207+
208+
counter++;
209+
210+
// Short circuit here because we don't need to check over 2;
211+
if (counter > 2) {
212+
return counter;
213+
}
214+
}
215+
}
216+
}

0 commit comments

Comments
 (0)