From c3265d7930557ec0596e07f72e39f1acb501c3ae Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Tue, 7 May 2024 15:45:50 +0530 Subject: [PATCH] chore: fix duplicate file name error when exporting notes and refactor file name utils (#2877) [skip e2e] --- packages/filepicker/src/utils.spec.ts | 3 +- packages/filepicker/src/utils.ts | 12 ------ .../ui-services/src/Archive/ArchiveManager.ts | 24 ++---------- .../src/Import/HTMLConverter/HTMLConverter.ts | 2 +- packages/ui-services/src/Import/Importer.ts | 2 +- .../PlaintextConverter/PlaintextConverter.ts | 2 +- .../Import/SuperConverter/SuperConverter.ts | 2 +- .../src/Domain/FileName/FileNameUtils.ts | 38 +++++++++++++++++++ packages/utils/src/Domain/index.ts | 1 + .../FilePreview/PreviewComponent.tsx | 3 +- .../Preferences/Panes/Backups/DataBackups.tsx | 3 +- .../Lexical/Nodes/FileExportNode.tsx | 2 +- .../Tools/HeadlessSuperConverter.tsx | 2 +- .../Controllers/FilesController.ts | 10 +---- .../NativeMobileWeb/DownloadBlobOnAndroid.tsx | 3 +- .../src/javascripts/Utils/NoteExportUtils.ts | 8 ++-- 16 files changed, 61 insertions(+), 56 deletions(-) create mode 100644 packages/utils/src/Domain/FileName/FileNameUtils.ts diff --git a/packages/filepicker/src/utils.spec.ts b/packages/filepicker/src/utils.spec.ts index c0e92df9ea8..ee67c31ecc9 100644 --- a/packages/filepicker/src/utils.spec.ts +++ b/packages/filepicker/src/utils.spec.ts @@ -1,4 +1,5 @@ -import { formatSizeToReadableString, parseFileName } from './utils' +import { formatSizeToReadableString } from './utils' +import { parseFileName } from '@standardnotes/utils' describe('utils', () => { describe('parseFileName', () => { diff --git a/packages/filepicker/src/utils.ts b/packages/filepicker/src/utils.ts index ab35b1db905..12619c0b393 100644 --- a/packages/filepicker/src/utils.ts +++ b/packages/filepicker/src/utils.ts @@ -10,18 +10,6 @@ export async function readFile(file: File): Promise { }) } -export function parseFileName(fileName: string): { - name: string - ext: string -} { - const pattern = /(?:\.([^.]+))?$/ - const extMatches = pattern.exec(fileName) - const ext = extMatches?.[1] || '' - const name = fileName.includes('.') ? fileName.substring(0, fileName.lastIndexOf('.')) : fileName - - return { name, ext } -} - export function saveFile(name: string, bytes: Uint8Array): void { const link = document.createElement('a') const blob = new Blob([bytes], { diff --git a/packages/ui-services/src/Archive/ArchiveManager.ts b/packages/ui-services/src/Archive/ArchiveManager.ts index 62f15f55d47..f204dffb083 100644 --- a/packages/ui-services/src/Archive/ArchiveManager.ts +++ b/packages/ui-services/src/Archive/ArchiveManager.ts @@ -1,4 +1,4 @@ -import { parseFileName } from '@standardnotes/filepicker' +import { parseFileName, createZippableFileName, sanitizeFileName } from '@standardnotes/utils' import { BackupFile, BackupFileDecryptedContextualPayload, @@ -8,22 +8,6 @@ import { import { ContentType } from '@standardnotes/domain-core' import { ApplicationInterface } from '@standardnotes/services' -export function sanitizeFileName(name: string): string { - return name.trim().replace(/[.\\/:"?*|<>]/g, '_') -} - -function zippableFileName(name: string, suffix = '', format = 'txt'): string { - const sanitizedName = sanitizeFileName(name) - const nameEnd = suffix + '.' + format - const maxFileNameLength = 100 - return sanitizedName.slice(0, maxFileNameLength - nameEnd.length) + nameEnd -} - -export function parseAndCreateZippableFileName(name: string, suffix = '') { - const { name: parsedName, ext } = parseFileName(name) - return zippableFileName(parsedName, suffix, ext) -} - type ZippableData = { name: string content: Blob @@ -87,7 +71,7 @@ export class ArchiveManager { type: 'text/plain', }) - const fileName = zippableFileName('Standard Notes Backup and Import File') + const fileName = createZippableFileName('Standard Notes Backup and Import File') await zipWriter.add(fileName, new zip.BlobReader(blob)) for (let index = 0; index < items.length; index++) { @@ -109,7 +93,7 @@ export class ArchiveManager { const blob = new Blob([contents], { type: 'text/plain' }) const fileName = - `Items/${sanitizeFileName(item.content_type)}/` + zippableFileName(name, `-${item.uuid.split('-')[0]}`) + `Items/${sanitizeFileName(item.content_type)}/` + createZippableFileName(name, `-${item.uuid.split('-')[0]}`) await zipWriter.add(fileName, new zip.BlobReader(blob)) } @@ -137,7 +121,7 @@ export class ArchiveManager { const currentFileNameIndex = filenameCounts[file.name] await writer.add( - zippableFileName(name, currentFileNameIndex > 0 ? ` - ${currentFileNameIndex}` : '', ext), + createZippableFileName(name, currentFileNameIndex > 0 ? ` - ${currentFileNameIndex}` : '', ext), new zip.BlobReader(file.content), ) } diff --git a/packages/ui-services/src/Import/HTMLConverter/HTMLConverter.ts b/packages/ui-services/src/Import/HTMLConverter/HTMLConverter.ts index 5aa12707812..a748ed6931c 100644 --- a/packages/ui-services/src/Import/HTMLConverter/HTMLConverter.ts +++ b/packages/ui-services/src/Import/HTMLConverter/HTMLConverter.ts @@ -1,4 +1,4 @@ -import { parseFileName } from '@standardnotes/filepicker' +import { parseFileName } from '@standardnotes/utils' import { Converter } from '../Converter' export class HTMLConverter implements Converter { diff --git a/packages/ui-services/src/Import/Importer.ts b/packages/ui-services/src/Import/Importer.ts index 3d21886fa24..a8af94e8da0 100644 --- a/packages/ui-services/src/Import/Importer.ts +++ b/packages/ui-services/src/Import/Importer.ts @@ -1,4 +1,4 @@ -import { parseFileName } from '@standardnotes/filepicker' +import { parseFileName } from '@standardnotes/utils' import { FeatureStatus, FeaturesClientInterface, diff --git a/packages/ui-services/src/Import/PlaintextConverter/PlaintextConverter.ts b/packages/ui-services/src/Import/PlaintextConverter/PlaintextConverter.ts index 6b4097a26c6..3e478827d6e 100644 --- a/packages/ui-services/src/Import/PlaintextConverter/PlaintextConverter.ts +++ b/packages/ui-services/src/Import/PlaintextConverter/PlaintextConverter.ts @@ -1,4 +1,4 @@ -import { parseFileName } from '@standardnotes/filepicker' +import { parseFileName } from '@standardnotes/utils' import { Converter } from '../Converter' export class PlaintextConverter implements Converter { diff --git a/packages/ui-services/src/Import/SuperConverter/SuperConverter.ts b/packages/ui-services/src/Import/SuperConverter/SuperConverter.ts index c81b037c401..451804ea3eb 100644 --- a/packages/ui-services/src/Import/SuperConverter/SuperConverter.ts +++ b/packages/ui-services/src/Import/SuperConverter/SuperConverter.ts @@ -1,5 +1,5 @@ import { SuperConverterServiceInterface } from '@standardnotes/files' -import { parseFileName } from '@standardnotes/filepicker' +import { parseFileName } from '@standardnotes/utils' import { Converter } from '../Converter' import { ConversionResult } from '../ConversionResult' diff --git a/packages/utils/src/Domain/FileName/FileNameUtils.ts b/packages/utils/src/Domain/FileName/FileNameUtils.ts new file mode 100644 index 00000000000..8d849d5ddeb --- /dev/null +++ b/packages/utils/src/Domain/FileName/FileNameUtils.ts @@ -0,0 +1,38 @@ +export function parseFileName(fileName: string): { + name: string + ext: string +} { + const pattern = /(?:\.([^.]+))$/ + const extMatches = pattern.exec(fileName) + const ext = extMatches?.[1] || '' + const name = fileName.includes('.') ? fileName.substring(0, fileName.lastIndexOf('.')) : fileName + + return { name, ext } +} + +export function sanitizeFileName(name: string): string { + return name.trim().replace(/[.\\/:"?*|<>]/g, '_') +} + +export function truncateFileName(name: string, maxLength: number): string { + return name.length > maxLength ? name.slice(0, maxLength) : name +} + +const MaxFileNameLength = 100 + +export function createZippableFileName( + name: string, + suffix = '', + format = 'txt', + maxLength = MaxFileNameLength, +): string { + const sanitizedName = sanitizeFileName(name) + const truncatedName = truncateFileName(sanitizedName, maxLength) + const nameEnd = suffix + '.' + format + return truncatedName + nameEnd +} + +export function parseAndCreateZippableFileName(name: string, suffix = '') { + const { name: parsedName, ext } = parseFileName(name) + return createZippableFileName(parsedName, suffix, ext) +} diff --git a/packages/utils/src/Domain/index.ts b/packages/utils/src/Domain/index.ts index 21fbfdbe740..1d15f74b9ae 100644 --- a/packages/utils/src/Domain/index.ts +++ b/packages/utils/src/Domain/index.ts @@ -11,3 +11,4 @@ export * from './Utils/Utils' export * from './Uuid/Utils' export * from './Uuid/UuidGenerator' export * from './Uuid/UuidMap' +export * from './FileName/FileNameUtils' diff --git a/packages/web/src/javascripts/Components/FilePreview/PreviewComponent.tsx b/packages/web/src/javascripts/Components/FilePreview/PreviewComponent.tsx index 5c8ff97bac3..85534c37f25 100644 --- a/packages/web/src/javascripts/Components/FilePreview/PreviewComponent.tsx +++ b/packages/web/src/javascripts/Components/FilePreview/PreviewComponent.tsx @@ -8,8 +8,7 @@ import ImagePreview from './ImagePreview' import { ImageZoomLevelProps } from './ImageZoomLevelProps' import { PreviewableTextFileTypes, RequiresNativeFilePreview } from './isFilePreviewable' import TextPreview from './TextPreview' -import { parseFileName } from '@standardnotes/filepicker' -import { sanitizeFileName } from '@standardnotes/ui-services' +import { parseFileName, sanitizeFileName } from '@standardnotes/utils' import VideoPreview from './VideoPreview' type Props = { diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Backups/DataBackups.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Backups/DataBackups.tsx index 661c6b25126..f1bd12b8da6 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Backups/DataBackups.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Backups/DataBackups.tsx @@ -1,4 +1,4 @@ -import { alertDialog, sanitizeFileName } from '@standardnotes/ui-services' +import { alertDialog } from '@standardnotes/ui-services' import { STRING_IMPORT_SUCCESS, STRING_INVALID_IMPORT_FILE, @@ -10,6 +10,7 @@ import { STRING_ENC_NOT_ENABLED, } from '@/Constants/Strings' import { BackupFile } from '@standardnotes/snjs' +import { sanitizeFileName } from '@standardnotes/utils' import { ChangeEventHandler, MouseEventHandler, useCallback, useEffect, useRef, useState } from 'react' import { WebApplication } from '@/Application/WebApplication' import { observer } from 'mobx-react-lite' diff --git a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Nodes/FileExportNode.tsx b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Nodes/FileExportNode.tsx index 4206e836fb2..6b0cc05ff16 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Nodes/FileExportNode.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Nodes/FileExportNode.tsx @@ -1,5 +1,5 @@ import { DecoratorBlockNode, SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode' -import { parseAndCreateZippableFileName } from '@standardnotes/ui-services' +import { parseAndCreateZippableFileName } from '@standardnotes/utils' import { DOMExportOutput, Spread } from 'lexical' type SerializedFileExportNode = Spread< diff --git a/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx b/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx index d43e698337f..0482fe49f07 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx @@ -18,7 +18,7 @@ import { $createFileExportNode } from '../Lexical/Nodes/FileExportNode' import { $createInlineFileNode } from '../Plugins/InlineFilePlugin/InlineFileNode' import { $convertFromMarkdownString } from '../Lexical/Utils/MarkdownImport' import { $convertToMarkdownString } from '../Lexical/Utils/MarkdownExport' -import { parseFileName } from '@standardnotes/filepicker' +import { parseFileName } from '@standardnotes/utils' export class HeadlessSuperConverter implements SuperConverterServiceInterface { private importEditor: LexicalEditor diff --git a/packages/web/src/javascripts/Controllers/FilesController.ts b/packages/web/src/javascripts/Controllers/FilesController.ts index 35998571ddc..a9c3ad4eb61 100644 --- a/packages/web/src/javascripts/Controllers/FilesController.ts +++ b/packages/web/src/javascripts/Controllers/FilesController.ts @@ -11,18 +11,12 @@ import { ArchiveManager, confirmDialog, IsNativeMobileWeb, - parseAndCreateZippableFileName, VaultDisplayServiceInterface, } from '@standardnotes/ui-services' import { Strings, StringUtils } from '@/Constants/Strings' import { concatenateUint8Arrays } from '@/Utils/ConcatenateUint8Arrays' -import { - ClassicFileReader, - StreamingFileReader, - StreamingFileSaver, - ClassicFileSaver, - parseFileName, -} from '@standardnotes/filepicker' +import { ClassicFileReader, StreamingFileReader, StreamingFileSaver, ClassicFileSaver } from '@standardnotes/filepicker' +import { parseAndCreateZippableFileName, parseFileName } from '@standardnotes/utils' import { AlertService, ChallengeReason, diff --git a/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx b/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx index fdd705c1796..96a536b840b 100644 --- a/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx +++ b/packages/web/src/javascripts/NativeMobileWeb/DownloadBlobOnAndroid.tsx @@ -1,8 +1,7 @@ import { getBase64FromBlob } from '@/Utils' -import { parseFileName } from '@standardnotes/filepicker' +import { parseFileName, sanitizeFileName } from '@standardnotes/utils' import { MobileDeviceInterface } from '@standardnotes/snjs' import { addToast, ToastType, dismissToast } from '@standardnotes/toast' -import { sanitizeFileName } from '@standardnotes/ui-services' export const downloadBlobOnAndroid = async ( mobileDevice: MobileDeviceInterface, diff --git a/packages/web/src/javascripts/Utils/NoteExportUtils.ts b/packages/web/src/javascripts/Utils/NoteExportUtils.ts index 573eee005b8..38369e001c8 100644 --- a/packages/web/src/javascripts/Utils/NoteExportUtils.ts +++ b/packages/web/src/javascripts/Utils/NoteExportUtils.ts @@ -1,8 +1,8 @@ import { WebApplication } from '@/Application/WebApplication' import { HeadlessSuperConverter } from '@/Components/SuperEditor/Tools/HeadlessSuperConverter' import { NoteType, PrefKey, SNNote, PrefDefaults, FileItem, PrefValue } from '@standardnotes/snjs' -import { WebApplicationInterface, parseAndCreateZippableFileName } from '@standardnotes/ui-services' -import { ZipDirectoryEntry } from '@zip.js/zip.js' +import { WebApplicationInterface } from '@standardnotes/ui-services' +import { type ZipDirectoryEntry } from '@zip.js/zip.js' // @ts-expect-error Using inline loaders to load CSS as string import superEditorCSS from '!css-loader?{"sourceMap":false}!sass-loader!../Components/SuperEditor/Lexical/Theme/editor.scss' // @ts-expect-error Using inline loaders to load CSS as string @@ -10,7 +10,7 @@ import snColorsCSS from '!css-loader?{"sourceMap":false}!sass-loader!@standardno // @ts-expect-error Using inline loaders to load CSS as string import exportOverridesCSS from '!css-loader?{"sourceMap":false}!sass-loader!../Components/SuperEditor/Lexical/Theme/export-overrides.scss' import { getBase64FromBlob } from './Utils' -import { parseFileName } from '@standardnotes/filepicker' +import { parseFileName, parseAndCreateZippableFileName } from '@standardnotes/utils' export const getNoteFormat = (application: WebApplicationInterface, note: SNNote) => { if (note.noteType === NoteType.Super) { @@ -238,7 +238,7 @@ export const createNoteExport = async ( for (const note of notes) { const blob = await getNoteBlob(application, note, superEmbedBehaviorPref) - const _name = getNoteFileName(application, note) + const _name = parseAndCreateZippableFileName(getNoteFileName(application, note)) filenameCounts[_name] = filenameCounts[_name] == undefined ? 0 : filenameCounts[_name] + 1