diff --git a/packages/server/lib/screenshots.js b/packages/server/lib/screenshots.js index 58175b4e5683..f149731d5798 100644 --- a/packages/server/lib/screenshots.js +++ b/packages/server/lib/screenshots.js @@ -18,6 +18,14 @@ const pathSeparatorRe = /[\\\/]/g // internal id incrementor let __ID__ = null +// many filesystems limit filename length to 255 bytes/characters, so truncate the filename to +// the smallest common denominator of safe filenames, which is 255 bytes. when ENAMETOOLONG +// errors are encountered, `maxSafeBytes` will be decremented to at most `MIN_PREFIX_BYTES`, at +// which point the latest ENAMTOOLONG error will be emitted. +// @see https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits +let maxSafeBytes = 255 +const MIN_PREFIX_BYTES = 64 + // TODO: when we parallelize these builds we'll need // a semaphore to access the file system when we write // screenshots since its possible two screenshots with @@ -288,17 +296,13 @@ const getDimensions = function (details) { return pick(details.image.bitmap) } -const ensureUniquePath = function (withoutExt, extension, num = 0) { +const ensureSafePath = function (withoutExt, extension, num = 0) { const suffix = `${num ? ` (${num})` : ''}.${extension}` - // many filesystems limit filename length to 255 bytes/characters, so truncate the filename to - // the smallest common denominator of safe filenames, which is 255 bytes - // @see https://github.com/cypress-io/cypress/issues/2403 - // @see https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits - const maxSafeBytes = 255 - suffix.length + const maxSafePrefixBytes = maxSafeBytes - suffix.length const filenameBuf = Buffer.from(path.basename(withoutExt)) - if (filenameBuf.byteLength > maxSafeBytes) { - const truncated = filenameBuf.slice(0, maxSafeBytes).toString() + if (filenameBuf.byteLength > maxSafePrefixBytes) { + const truncated = filenameBuf.slice(0, maxSafePrefixBytes).toString() withoutExt = path.join(path.dirname(withoutExt), truncated) } @@ -308,10 +312,21 @@ const ensureUniquePath = function (withoutExt, extension, num = 0) { return fs.pathExists(fullPath) .then((found) => { if (found) { - return ensureUniquePath(withoutExt, extension, num + 1) + return ensureSafePath(withoutExt, extension, num + 1) } - return fullPath + // path does not exist, attempt to create it to check for an ENAMETOOLONG error + return fs.outputFileAsync(fullPath, '') + .then(() => fullPath) + .catch((err) => { + if (err.code === 'ENAMETOOLONG' && maxSafePrefixBytes >= MIN_PREFIX_BYTES) { + maxSafeBytes -= 1 + + return ensureSafePath(withoutExt, extension, num) + } + + throw err + }) }) } @@ -346,7 +361,7 @@ const getPath = function (data, ext, screenshotsFolder) { const withoutExt = path.join(screenshotsFolder, ...specNames, ...names) - return ensureUniquePath(withoutExt, ext) + return ensureSafePath(withoutExt, ext) } const getPathToScreenshot = function (data, details, screenshotsFolder) {