import dayjs from 'dayjs' import { deburr } from 'lodash' import { IMI_ORGANIZATIONS } from './config/imi' import { AttributeFileValue, ExternalFile, isExternalFile, isRemoteFile, } from './interfaces' export const removeExtension = (filename: string) => { return filename.substring(0, filename.lastIndexOf('.')) || filename } export const getExtension = (filename: string) => { return filename.substring(filename.lastIndexOf('.') + 0) } export const FILE_TIMESTAMP_REGEX = /\s\(\d{2}\.\d{2}\.\d{2}.+\)/gm // Matches an MD5-looking prefix ("<32 hex chars>.") that shouldn't be part of a // user-facing filename. Local cache files are sometimes named this way internally // (hash-prefixed, for on-device dedup); if one is ever picked up as an upload, this // keeps the prefix from being carried forward into every later renamed revision. const LEAKED_HASH_PREFIX_REGEX = /^[0-9a-f]{32}\./i const stripLeakedHashPrefix = (filename: string): string => filename.replace(LEAKED_HASH_PREFIX_REGEX, '') export const findFileNameConflicts = ( organization: string, original: (AttributeFileValue | ExternalFile)[], added: string[], ): { added: string[]; conflictHashes: string[] } => { return IMI_ORGANIZATIONS.includes(organization) ? resolveFileNameConfictsIMI(original, added) : resolveFileNameConflictsGlobal(original, added) } export const resolveFileNameConflictsGlobal = ( original: (AttributeFileValue | ExternalFile)[], added: string[], ): { added: string[]; conflictHashes: string[] } => { const conflictHashes: string[] = [] const concated = original .map((f) => (isRemoteFile(f) ? f.file.filename : f.name)) .concat(added) const count: { [key: string]: number } = {} concated.forEach(function (x, i) { if (concated.indexOf(x) !== i) { const c = x in count ? (count[x] = count[x] + 1) : (count[x] = 1) const withoutExtension = removeExtension(x) const extension = getExtension(x) let j = c + 1 let newName = withoutExtension + '(' + j + ')' + extension while (concated.indexOf(newName) !== -1) { newName = withoutExtension + '(' + ++j + ')' + extension } const originalFile = original.find((f) => isRemoteFile(f) ? f.file.filename === x : f.name === x, ) // skip checking for conflicts in external files if (originalFile && isRemoteFile(originalFile)) { conflictHashes.push(originalFile.file.hash) } concated[i] = newName } }) concated.splice(0, original.length) return { added: concated, conflictHashes, } } export const resolveFileNameConfictsIMI = ( original: (AttributeFileValue | ExternalFile)[], added: string[], ): { added: string[]; conflictHashes: string[] } => { const conflictHashes: string[] = [] const addedChanged = added.map((file) => { const cleanedFileName = stripLeakedHashPrefix( deburr(file.replace(FILE_TIMESTAMP_REGEX, '')), ) let hasConflict = false original.forEach((f) => { if (isRemoteFile(f)) { const cleanedOldFilename = stripLeakedHashPrefix( deburr(f.file.filename.replace(FILE_TIMESTAMP_REGEX, '')), ) const isConflict = cleanedFileName === cleanedOldFilename if (isConflict && !conflictHashes.includes(f.file.hash)) { conflictHashes.push(f.file.hash) hasConflict = true } } else if (isExternalFile(f)) { const cleanedOldFilename = stripLeakedHashPrefix( deburr(f.name.replace(FILE_TIMESTAMP_REGEX, '')), ) const isConflict = cleanedFileName === cleanedOldFilename if (isConflict && !conflictHashes.includes(f.md5)) { // external files are not stored in the database, so we do not compute hash conflicts hasConflict = true } } }) if (hasConflict) { const timestamp = dayjs().format('DD.MM.YY HH:mm:ss') const ext = getExtension(cleanedFileName) const firstPart = removeExtension(cleanedFileName) return `${firstPart} (${timestamp})${ext}` } else { return file } }) return { added: addedChanged, conflictHashes, } }