Skip to content

Commit b0b3895

Browse files
authored
Feature/engg 3986 local workspace file names should be readable (#210)
1 parent 701e28e commit b0b3895

File tree

11 files changed

+689
-224
lines changed

11 files changed

+689
-224
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "requestly",
33
"productName": "Requestly",
4-
"version": "25.10.3",
4+
"version": "25.10.8",
55
"main": "src/main/main.ts",
66
"private": true,
77
"description": "Intercept & Modify HTTP Requests",
@@ -343,4 +343,4 @@
343343
"pre-commit": "lint-staged"
344344
}
345345
}
346-
}
346+
}

release/app/package-lock.json

Lines changed: 2 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

release/app/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "requestly",
33
"productName": "Requestly",
4-
"version": "25.10.3",
4+
"version": "25.10.8",
55
"private": true,
66
"description": "Intercept & Modify HTTP Requests",
77
"main": "./dist/main/main.js",
@@ -34,4 +34,4 @@
3434
"bufferutil": "4.0.3",
3535
"utf-8-validate": "5.0.6"
3636
}
37-
}
37+
}

src/renderer/actions/local-sync/common-utils.ts

Lines changed: 161 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
FileTypeEnum,
99
FsResource,
1010
} from "./types";
11+
import { fileIndex } from "./file-index";
1112

1213
export class FsResourceCreationError extends Error {
1314
path: string;
@@ -32,6 +33,7 @@ export function createFsResource<T extends FsResource["type"]>(params: {
3233
rootPath: string;
3334
path: string;
3435
type: T;
36+
exposedWorkspacePaths?: Map<string, unknown>,
3537
}): FsResource & { type: T } {
3638
try {
3739
const { rootPath, type } = params;
@@ -47,12 +49,11 @@ export function createFsResource<T extends FsResource["type"]>(params: {
4749
);
4850
}
4951

50-
const normalizedRootPath = getNormalizedPath(rootPath);
51-
const pathRootSlice = path.slice(0, normalizedRootPath.length);
52-
53-
if (normalizedRootPath !== pathRootSlice) {
52+
const normalizedRootPaths = new Set<string>().add(getNormalizedPath(rootPath));
53+
params.exposedWorkspacePaths?.keys().forEach(path => normalizedRootPaths.add(getNormalizedPath(path)));
54+
if (!normalizedRootPaths.values().some(rootPath => path.startsWith(rootPath))) {
5455
throw new Error(
55-
`Can not create fs resource reference! Path '${path}' lies outside workspace path '${rootPath}'`
56+
`Can not create fs resource reference! Path '${path}' lies outside workspace paths!`
5657
);
5758
}
5859

@@ -124,7 +125,8 @@ export function parseJsonContent<T extends TSchema>(
124125
}
125126

126127
export function getIdFromPath(path: string) {
127-
return path;
128+
const id = fileIndex.getId(path);
129+
return id;
128130
}
129131

130132
export function mapSuccessWrite<
@@ -217,3 +219,156 @@ export function createFileSystemError(
217219
},
218220
};
219221
}
222+
223+
/**
224+
* WARNING: Genrated by Claude
225+
*
226+
* Sanitizes a string to be safe for use as a filename or folder name across all platforms
227+
* (Windows, macOS, Linux)
228+
*
229+
* @param input - The input string to sanitize
230+
* @param maxLength - Maximum length for the filename (default: 100)
231+
* @param replacement - Character to replace invalid characters with (default: '_')
232+
* @returns A sanitized filename safe for all platforms
233+
*/
234+
export function sanitizeFsResourceName(
235+
input: string,
236+
maxLength: number = 100,
237+
replacement: string = '_'
238+
): string {
239+
if (!input || typeof input !== 'string') {
240+
return 'Untitled';
241+
}
242+
243+
// Trim whitespace
244+
let sanitized = input.trim();
245+
246+
// If empty after trim, return default
247+
if (!sanitized) {
248+
return 'Untitled';
249+
}
250+
251+
// Reserved names on Windows (case-insensitive)
252+
const reservedNames = new Set([
253+
'CON', 'PRN', 'AUX', 'NUL',
254+
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
255+
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'
256+
]);
257+
258+
// Characters that are invalid in filenames across platforms
259+
// Windows: < > : " | ? * \ /
260+
// macOS: : (converted to /)
261+
// Linux: / and null character
262+
// We'll be conservative and exclude all problematic characters
263+
const invalidCharsRegex = /[<>:"/\\|?*\x00-\x1F\x7F]/g;
264+
265+
// Replace invalid characters
266+
sanitized = sanitized.replace(invalidCharsRegex, replacement);
267+
268+
// Remove or replace problematic characters at start/end
269+
// Can't start or end with spaces or dots on Windows
270+
sanitized = sanitized.replace(/^[.\s]+|[.\s]+$/g, replacement);
271+
272+
// Handle multiple consecutive replacement characters
273+
if (replacement) {
274+
const replacementRegex = new RegExp(`${escapeRegex(replacement)}+`, 'g');
275+
sanitized = sanitized.replace(replacementRegex, replacement);
276+
}
277+
278+
// Check if it's a reserved name (Windows)
279+
const nameWithoutExt = sanitized.split('.')[0].toUpperCase();
280+
if (reservedNames.has(nameWithoutExt)) {
281+
sanitized = `${replacement}${sanitized}`;
282+
}
283+
284+
// Ensure it doesn't start with a dash (can cause issues with command line tools)
285+
if (sanitized.startsWith('-')) {
286+
sanitized = replacement + sanitized.slice(1);
287+
}
288+
289+
// Limit length while preserving file extension if present
290+
if (sanitized.length > maxLength) {
291+
const lastDotIndex = sanitized.lastIndexOf('.');
292+
if (lastDotIndex > 0 && lastDotIndex > sanitized.length - 10) {
293+
// Has extension, preserve it
294+
const extension = sanitized.slice(lastDotIndex);
295+
const nameOnly = sanitized.slice(0, lastDotIndex);
296+
const maxNameLength = maxLength - extension.length;
297+
sanitized = nameOnly.slice(0, maxNameLength) + extension;
298+
} else {
299+
// No extension or extension is too far back
300+
sanitized = sanitized.slice(0, maxLength);
301+
}
302+
}
303+
304+
// Final cleanup - remove trailing dots and spaces again (in case truncation caused issues)
305+
sanitized = sanitized.replace(/[.\s]+$/, '');
306+
307+
// If we ended up with an empty string, return default
308+
if (!sanitized) {
309+
return 'Untitled';
310+
}
311+
312+
return sanitized;
313+
}
314+
315+
/**
316+
* Helper function to escape special regex characters
317+
*/
318+
function escapeRegex(string: string): string {
319+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
320+
}
321+
322+
/**
323+
* Detects if a given name corresponds to a 'new' entity based on a base string pattern.
324+
* Matches exact base string or base string followed by a number.
325+
*
326+
* @param name - The name to check (e.g., 'Untitled', 'Untitled1', 'Untitled42')
327+
* @param baseString - The base string to match against (e.g., 'Untitled', 'New Environment')
328+
* @returns true if the name matches the pattern, false otherwise
329+
*/
330+
export function isNewEntityName(name: string, baseString: string): boolean {
331+
// Escape special regex characters in the base string
332+
const escapedBase = baseString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
333+
334+
// Pattern: exact match OR base string followed by one or more digits
335+
const pattern = new RegExp(`^${escapedBase}(\\d+)?$`);
336+
337+
return pattern.test(name);
338+
}
339+
340+
/**
341+
* Pure function that generates the next available name variant.
342+
* Always starts with baseName + "1" and increments until finding an available name.
343+
*
344+
* @param baseName - The base name to generate alternatives for
345+
* @param existingNames - Array of existing names to avoid conflicts with
346+
* @returns The next available name variant (e.g., 'Untitled1', 'Untitled2')
347+
*/
348+
export function getAlternateName(baseName: string, existingNames: Set<string>): string {
349+
if(!existingNames.has(baseName)) {
350+
return baseName;
351+
}
352+
let counter = 1;
353+
let candidateName = `${baseName}${counter}`;
354+
355+
while (existingNames.has(candidateName)) {
356+
counter++;
357+
candidateName = `${baseName}${counter}`;
358+
}
359+
360+
return candidateName;
361+
}
362+
363+
export function getNewNameIfQuickCreate(params: {
364+
name: string,
365+
baseName: string,
366+
parentPath: string,
367+
}) {
368+
if(!isNewEntityName(params.name, params.baseName)) {
369+
return params.name;
370+
}
371+
const children = fileIndex.getImmediateChildren(params.parentPath);
372+
373+
return getAlternateName(params.baseName, children);
374+
}

0 commit comments

Comments
 (0)