chore: fix duplicate file name error when exporting notes and refactor file name utils (#2877) [skip e2e]
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { formatSizeToReadableString, parseFileName } from './utils'
|
import { formatSizeToReadableString } from './utils'
|
||||||
|
import { parseFileName } from '@standardnotes/utils'
|
||||||
|
|
||||||
describe('utils', () => {
|
describe('utils', () => {
|
||||||
describe('parseFileName', () => {
|
describe('parseFileName', () => {
|
||||||
|
|||||||
@@ -10,18 +10,6 @@ export async function readFile(file: File): Promise<Uint8Array> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
export function saveFile(name: string, bytes: Uint8Array): void {
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
const blob = new Blob([bytes], {
|
const blob = new Blob([bytes], {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { parseFileName } from '@standardnotes/filepicker'
|
import { parseFileName, createZippableFileName, sanitizeFileName } from '@standardnotes/utils'
|
||||||
import {
|
import {
|
||||||
BackupFile,
|
BackupFile,
|
||||||
BackupFileDecryptedContextualPayload,
|
BackupFileDecryptedContextualPayload,
|
||||||
@@ -8,22 +8,6 @@ import {
|
|||||||
import { ContentType } from '@standardnotes/domain-core'
|
import { ContentType } from '@standardnotes/domain-core'
|
||||||
import { ApplicationInterface } from '@standardnotes/services'
|
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 = {
|
type ZippableData = {
|
||||||
name: string
|
name: string
|
||||||
content: Blob
|
content: Blob
|
||||||
@@ -87,7 +71,7 @@ export class ArchiveManager {
|
|||||||
type: 'text/plain',
|
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))
|
await zipWriter.add(fileName, new zip.BlobReader(blob))
|
||||||
|
|
||||||
for (let index = 0; index < items.length; index++) {
|
for (let index = 0; index < items.length; index++) {
|
||||||
@@ -109,7 +93,7 @@ export class ArchiveManager {
|
|||||||
|
|
||||||
const blob = new Blob([contents], { type: 'text/plain' })
|
const blob = new Blob([contents], { type: 'text/plain' })
|
||||||
const fileName =
|
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))
|
await zipWriter.add(fileName, new zip.BlobReader(blob))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +121,7 @@ export class ArchiveManager {
|
|||||||
const currentFileNameIndex = filenameCounts[file.name]
|
const currentFileNameIndex = filenameCounts[file.name]
|
||||||
|
|
||||||
await writer.add(
|
await writer.add(
|
||||||
zippableFileName(name, currentFileNameIndex > 0 ? ` - ${currentFileNameIndex}` : '', ext),
|
createZippableFileName(name, currentFileNameIndex > 0 ? ` - ${currentFileNameIndex}` : '', ext),
|
||||||
new zip.BlobReader(file.content),
|
new zip.BlobReader(file.content),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { parseFileName } from '@standardnotes/filepicker'
|
import { parseFileName } from '@standardnotes/utils'
|
||||||
import { Converter } from '../Converter'
|
import { Converter } from '../Converter'
|
||||||
|
|
||||||
export class HTMLConverter implements Converter {
|
export class HTMLConverter implements Converter {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { parseFileName } from '@standardnotes/filepicker'
|
import { parseFileName } from '@standardnotes/utils'
|
||||||
import {
|
import {
|
||||||
FeatureStatus,
|
FeatureStatus,
|
||||||
FeaturesClientInterface,
|
FeaturesClientInterface,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { parseFileName } from '@standardnotes/filepicker'
|
import { parseFileName } from '@standardnotes/utils'
|
||||||
import { Converter } from '../Converter'
|
import { Converter } from '../Converter'
|
||||||
|
|
||||||
export class PlaintextConverter implements Converter {
|
export class PlaintextConverter implements Converter {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SuperConverterServiceInterface } from '@standardnotes/files'
|
import { SuperConverterServiceInterface } from '@standardnotes/files'
|
||||||
import { parseFileName } from '@standardnotes/filepicker'
|
import { parseFileName } from '@standardnotes/utils'
|
||||||
import { Converter } from '../Converter'
|
import { Converter } from '../Converter'
|
||||||
import { ConversionResult } from '../ConversionResult'
|
import { ConversionResult } from '../ConversionResult'
|
||||||
|
|
||||||
|
|||||||
38
packages/utils/src/Domain/FileName/FileNameUtils.ts
Normal file
38
packages/utils/src/Domain/FileName/FileNameUtils.ts
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -11,3 +11,4 @@ export * from './Utils/Utils'
|
|||||||
export * from './Uuid/Utils'
|
export * from './Uuid/Utils'
|
||||||
export * from './Uuid/UuidGenerator'
|
export * from './Uuid/UuidGenerator'
|
||||||
export * from './Uuid/UuidMap'
|
export * from './Uuid/UuidMap'
|
||||||
|
export * from './FileName/FileNameUtils'
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ import ImagePreview from './ImagePreview'
|
|||||||
import { ImageZoomLevelProps } from './ImageZoomLevelProps'
|
import { ImageZoomLevelProps } from './ImageZoomLevelProps'
|
||||||
import { PreviewableTextFileTypes, RequiresNativeFilePreview } from './isFilePreviewable'
|
import { PreviewableTextFileTypes, RequiresNativeFilePreview } from './isFilePreviewable'
|
||||||
import TextPreview from './TextPreview'
|
import TextPreview from './TextPreview'
|
||||||
import { parseFileName } from '@standardnotes/filepicker'
|
import { parseFileName, sanitizeFileName } from '@standardnotes/utils'
|
||||||
import { sanitizeFileName } from '@standardnotes/ui-services'
|
|
||||||
import VideoPreview from './VideoPreview'
|
import VideoPreview from './VideoPreview'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { alertDialog, sanitizeFileName } from '@standardnotes/ui-services'
|
import { alertDialog } from '@standardnotes/ui-services'
|
||||||
import {
|
import {
|
||||||
STRING_IMPORT_SUCCESS,
|
STRING_IMPORT_SUCCESS,
|
||||||
STRING_INVALID_IMPORT_FILE,
|
STRING_INVALID_IMPORT_FILE,
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
STRING_ENC_NOT_ENABLED,
|
STRING_ENC_NOT_ENABLED,
|
||||||
} from '@/Constants/Strings'
|
} from '@/Constants/Strings'
|
||||||
import { BackupFile } from '@standardnotes/snjs'
|
import { BackupFile } from '@standardnotes/snjs'
|
||||||
|
import { sanitizeFileName } from '@standardnotes/utils'
|
||||||
import { ChangeEventHandler, MouseEventHandler, useCallback, useEffect, useRef, useState } from 'react'
|
import { ChangeEventHandler, MouseEventHandler, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { WebApplication } from '@/Application/WebApplication'
|
import { WebApplication } from '@/Application/WebApplication'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DecoratorBlockNode, SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
import { DecoratorBlockNode, SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||||
import { parseAndCreateZippableFileName } from '@standardnotes/ui-services'
|
import { parseAndCreateZippableFileName } from '@standardnotes/utils'
|
||||||
import { DOMExportOutput, Spread } from 'lexical'
|
import { DOMExportOutput, Spread } from 'lexical'
|
||||||
|
|
||||||
type SerializedFileExportNode = Spread<
|
type SerializedFileExportNode = Spread<
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { $createFileExportNode } from '../Lexical/Nodes/FileExportNode'
|
|||||||
import { $createInlineFileNode } from '../Plugins/InlineFilePlugin/InlineFileNode'
|
import { $createInlineFileNode } from '../Plugins/InlineFilePlugin/InlineFileNode'
|
||||||
import { $convertFromMarkdownString } from '../Lexical/Utils/MarkdownImport'
|
import { $convertFromMarkdownString } from '../Lexical/Utils/MarkdownImport'
|
||||||
import { $convertToMarkdownString } from '../Lexical/Utils/MarkdownExport'
|
import { $convertToMarkdownString } from '../Lexical/Utils/MarkdownExport'
|
||||||
import { parseFileName } from '@standardnotes/filepicker'
|
import { parseFileName } from '@standardnotes/utils'
|
||||||
|
|
||||||
export class HeadlessSuperConverter implements SuperConverterServiceInterface {
|
export class HeadlessSuperConverter implements SuperConverterServiceInterface {
|
||||||
private importEditor: LexicalEditor
|
private importEditor: LexicalEditor
|
||||||
|
|||||||
@@ -11,18 +11,12 @@ import {
|
|||||||
ArchiveManager,
|
ArchiveManager,
|
||||||
confirmDialog,
|
confirmDialog,
|
||||||
IsNativeMobileWeb,
|
IsNativeMobileWeb,
|
||||||
parseAndCreateZippableFileName,
|
|
||||||
VaultDisplayServiceInterface,
|
VaultDisplayServiceInterface,
|
||||||
} from '@standardnotes/ui-services'
|
} from '@standardnotes/ui-services'
|
||||||
import { Strings, StringUtils } from '@/Constants/Strings'
|
import { Strings, StringUtils } from '@/Constants/Strings'
|
||||||
import { concatenateUint8Arrays } from '@/Utils/ConcatenateUint8Arrays'
|
import { concatenateUint8Arrays } from '@/Utils/ConcatenateUint8Arrays'
|
||||||
import {
|
import { ClassicFileReader, StreamingFileReader, StreamingFileSaver, ClassicFileSaver } from '@standardnotes/filepicker'
|
||||||
ClassicFileReader,
|
import { parseAndCreateZippableFileName, parseFileName } from '@standardnotes/utils'
|
||||||
StreamingFileReader,
|
|
||||||
StreamingFileSaver,
|
|
||||||
ClassicFileSaver,
|
|
||||||
parseFileName,
|
|
||||||
} from '@standardnotes/filepicker'
|
|
||||||
import {
|
import {
|
||||||
AlertService,
|
AlertService,
|
||||||
ChallengeReason,
|
ChallengeReason,
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { getBase64FromBlob } from '@/Utils'
|
import { getBase64FromBlob } from '@/Utils'
|
||||||
import { parseFileName } from '@standardnotes/filepicker'
|
import { parseFileName, sanitizeFileName } from '@standardnotes/utils'
|
||||||
import { MobileDeviceInterface } from '@standardnotes/snjs'
|
import { MobileDeviceInterface } from '@standardnotes/snjs'
|
||||||
import { addToast, ToastType, dismissToast } from '@standardnotes/toast'
|
import { addToast, ToastType, dismissToast } from '@standardnotes/toast'
|
||||||
import { sanitizeFileName } from '@standardnotes/ui-services'
|
|
||||||
|
|
||||||
export const downloadBlobOnAndroid = async (
|
export const downloadBlobOnAndroid = async (
|
||||||
mobileDevice: MobileDeviceInterface,
|
mobileDevice: MobileDeviceInterface,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { WebApplication } from '@/Application/WebApplication'
|
import { WebApplication } from '@/Application/WebApplication'
|
||||||
import { HeadlessSuperConverter } from '@/Components/SuperEditor/Tools/HeadlessSuperConverter'
|
import { HeadlessSuperConverter } from '@/Components/SuperEditor/Tools/HeadlessSuperConverter'
|
||||||
import { NoteType, PrefKey, SNNote, PrefDefaults, FileItem, PrefValue } from '@standardnotes/snjs'
|
import { NoteType, PrefKey, SNNote, PrefDefaults, FileItem, PrefValue } from '@standardnotes/snjs'
|
||||||
import { WebApplicationInterface, parseAndCreateZippableFileName } from '@standardnotes/ui-services'
|
import { WebApplicationInterface } from '@standardnotes/ui-services'
|
||||||
import { ZipDirectoryEntry } from '@zip.js/zip.js'
|
import { type ZipDirectoryEntry } from '@zip.js/zip.js'
|
||||||
// @ts-expect-error Using inline loaders to load CSS as string
|
// @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'
|
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
|
// @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
|
// @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 exportOverridesCSS from '!css-loader?{"sourceMap":false}!sass-loader!../Components/SuperEditor/Lexical/Theme/export-overrides.scss'
|
||||||
import { getBase64FromBlob } from './Utils'
|
import { getBase64FromBlob } from './Utils'
|
||||||
import { parseFileName } from '@standardnotes/filepicker'
|
import { parseFileName, parseAndCreateZippableFileName } from '@standardnotes/utils'
|
||||||
|
|
||||||
export const getNoteFormat = (application: WebApplicationInterface, note: SNNote) => {
|
export const getNoteFormat = (application: WebApplicationInterface, note: SNNote) => {
|
||||||
if (note.noteType === NoteType.Super) {
|
if (note.noteType === NoteType.Super) {
|
||||||
@@ -238,7 +238,7 @@ export const createNoteExport = async (
|
|||||||
|
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
const blob = await getNoteBlob(application, note, superEmbedBehaviorPref)
|
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
|
filenameCounts[_name] = filenameCounts[_name] == undefined ? 0 : filenameCounts[_name] + 1
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user