Save attachments with macOS quarantine attribute
* Attachments: Always save file to downloads directory, show toast * Add new build:dev command for casual builds
This commit is contained in:
parent
65befde0fa
commit
1bf9ca7233
11 changed files with 202 additions and 52 deletions
|
@ -782,6 +782,16 @@
|
||||||
"message": "A voice message must have only one attachment.",
|
"message": "A voice message must have only one attachment.",
|
||||||
"description": "Shown in toast if tries to record a voice note with any staged attachments"
|
"description": "Shown in toast if tries to record a voice note with any staged attachments"
|
||||||
},
|
},
|
||||||
|
"attachmentSavedToDownloads": {
|
||||||
|
"message": "Attachment saved as \"$name$\" in your Downloads folder. Click to show.",
|
||||||
|
"description": "Shown after user selects to save to downloads",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "proof.jpg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"you": {
|
"you": {
|
||||||
"message": "You",
|
"message": "You",
|
||||||
"description": "In Android theme, shown in quote if you or someone else replies to you"
|
"description": "In Android theme, shown in quote if you or someone else replies to you"
|
||||||
|
|
|
@ -1,11 +1,22 @@
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { app, shell, remote } = require('electron');
|
||||||
|
|
||||||
const pify = require('pify');
|
const pify = require('pify');
|
||||||
const glob = require('glob');
|
const glob = require('glob');
|
||||||
const fse = require('fs-extra');
|
const fse = require('fs-extra');
|
||||||
const toArrayBuffer = require('to-arraybuffer');
|
const toArrayBuffer = require('to-arraybuffer');
|
||||||
const { map, isArrayBuffer, isString } = require('lodash');
|
const { map, isArrayBuffer, isString } = require('lodash');
|
||||||
|
const sanitizeFilename = require('sanitize-filename');
|
||||||
|
const getGuid = require('uuid/v4');
|
||||||
|
|
||||||
|
let xattr;
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line global-require, import/no-extraneous-dependencies
|
||||||
|
xattr = require('fs-xattr');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('x-attr dependncy did not load successfully');
|
||||||
|
}
|
||||||
|
|
||||||
const PATH = 'attachments.noindex';
|
const PATH = 'attachments.noindex';
|
||||||
const STICKER_PATH = 'stickers.noindex';
|
const STICKER_PATH = 'stickers.noindex';
|
||||||
|
@ -153,6 +164,70 @@ exports.copyIntoAttachmentsDirectory = root => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.writeToDownloads = async ({ data, name }) => {
|
||||||
|
const appToUse = app || remote.app;
|
||||||
|
const downloadsPath =
|
||||||
|
appToUse.getPath('downloads') || appToUse.getPath('home');
|
||||||
|
const sanitized = sanitizeFilename(name);
|
||||||
|
|
||||||
|
const extension = path.extname(sanitized);
|
||||||
|
const basename = path.basename(sanitized, extension);
|
||||||
|
const getCandidateName = count => `${basename} (${count})${extension}`;
|
||||||
|
|
||||||
|
const existingFiles = await fse.readdir(downloadsPath);
|
||||||
|
let candidateName = sanitized;
|
||||||
|
let count = 0;
|
||||||
|
while (existingFiles.includes(candidateName)) {
|
||||||
|
count += 1;
|
||||||
|
candidateName = getCandidateName(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = path.join(downloadsPath, candidateName);
|
||||||
|
const normalized = path.normalize(target);
|
||||||
|
if (!normalized.startsWith(downloadsPath)) {
|
||||||
|
throw new Error('Invalid filename!');
|
||||||
|
}
|
||||||
|
|
||||||
|
await fse.writeFile(normalized, Buffer.from(data));
|
||||||
|
|
||||||
|
if (process.platform === 'darwin' && xattr) {
|
||||||
|
// kLSQuarantineTypeInstantMessageAttachment
|
||||||
|
const type = '0003';
|
||||||
|
|
||||||
|
// Hexadecimal seconds since epoch
|
||||||
|
const timestamp = Math.trunc(Date.now() / 1000).toString(16);
|
||||||
|
|
||||||
|
const appName = 'Signal';
|
||||||
|
const guid = getGuid();
|
||||||
|
|
||||||
|
// https://ilostmynotes.blogspot.com/2012/06/gatekeeper-xprotect-and-quarantine.html
|
||||||
|
const attrValue = `${type};${timestamp};${appName};${guid}`;
|
||||||
|
|
||||||
|
await xattr.set(normalized, 'com.apple.quarantine', attrValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fullPath: normalized,
|
||||||
|
name: candidateName,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.openFileInDownloads = async name => {
|
||||||
|
const shellToUse = shell || remote.shell;
|
||||||
|
const appToUse = app || remote.app;
|
||||||
|
|
||||||
|
const downloadsPath =
|
||||||
|
appToUse.getPath('downloads') || appToUse.getPath('home');
|
||||||
|
const target = path.join(downloadsPath, name);
|
||||||
|
|
||||||
|
const normalized = path.normalize(target);
|
||||||
|
if (!normalized.startsWith(downloadsPath)) {
|
||||||
|
throw new Error('Invalid filename!');
|
||||||
|
}
|
||||||
|
|
||||||
|
shellToUse.showItemInFolder(normalized);
|
||||||
|
};
|
||||||
|
|
||||||
// createWriterForNew :: AttachmentsPath ->
|
// createWriterForNew :: AttachmentsPath ->
|
||||||
// ArrayBuffer ->
|
// ArrayBuffer ->
|
||||||
// IO (Promise RelativePath)
|
// IO (Promise RelativePath)
|
||||||
|
|
|
@ -119,6 +119,8 @@ function initializeMigrations({
|
||||||
getPath,
|
getPath,
|
||||||
getStickersPath,
|
getStickersPath,
|
||||||
getTempPath,
|
getTempPath,
|
||||||
|
openFileInDownloads,
|
||||||
|
writeToDownloads,
|
||||||
} = Attachments;
|
} = Attachments;
|
||||||
const {
|
const {
|
||||||
getImageDimensions,
|
getImageDimensions,
|
||||||
|
@ -187,11 +189,13 @@ function initializeMigrations({
|
||||||
loadPreviewData,
|
loadPreviewData,
|
||||||
loadQuoteData,
|
loadQuoteData,
|
||||||
loadStickerData,
|
loadStickerData,
|
||||||
|
openFileInDownloads,
|
||||||
readAttachmentData,
|
readAttachmentData,
|
||||||
readDraftData,
|
readDraftData,
|
||||||
readStickerData,
|
readStickerData,
|
||||||
readTempData,
|
readTempData,
|
||||||
run,
|
run,
|
||||||
|
writeToDownloads,
|
||||||
processNewAttachment: attachment =>
|
processNewAttachment: attachment =>
|
||||||
MessageType.processNewAttachment(attachment, {
|
MessageType.processNewAttachment(attachment, {
|
||||||
writeNewAttachmentData,
|
writeNewAttachmentData,
|
||||||
|
|
|
@ -26,8 +26,11 @@
|
||||||
getAbsoluteTempPath,
|
getAbsoluteTempPath,
|
||||||
deleteDraftFile,
|
deleteDraftFile,
|
||||||
deleteTempFile,
|
deleteTempFile,
|
||||||
|
openFileInDownloads,
|
||||||
|
readAttachmentData,
|
||||||
readDraftData,
|
readDraftData,
|
||||||
writeNewDraftData,
|
writeNewDraftData,
|
||||||
|
writeToDownloads,
|
||||||
} = window.Signal.Migrations;
|
} = window.Signal.Migrations;
|
||||||
const {
|
const {
|
||||||
getOlderMessagesByConversation,
|
getOlderMessagesByConversation,
|
||||||
|
@ -87,6 +90,44 @@
|
||||||
return { toastMessage: i18n('conversationReturnedToInbox') };
|
return { toastMessage: i18n('conversationReturnedToInbox') };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Whisper.FileSavedToast = Whisper.ToastView.extend({
|
||||||
|
className: 'toast toast-clickable',
|
||||||
|
initialize(options) {
|
||||||
|
if (!options.name) {
|
||||||
|
throw new Error('FileSavedToast: name option was not provided!');
|
||||||
|
}
|
||||||
|
this.name = options.name;
|
||||||
|
this.timeout = 10000;
|
||||||
|
|
||||||
|
if (window.getInteractionMode() === 'keyboard') {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$el.focus();
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
click: 'onClick',
|
||||||
|
keydown: 'onKeydown',
|
||||||
|
},
|
||||||
|
onClick() {
|
||||||
|
openFileInDownloads(this.name);
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
onKeydown(event) {
|
||||||
|
if (event.key !== 'Enter' && event.key !== ' ') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
openFileInDownloads(this.name);
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
render_attributes() {
|
||||||
|
return { toastMessage: i18n('attachmentSavedToDownloads', this.name) };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
|
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
|
||||||
Whisper.MessageBodyTooLongToast = Whisper.ToastView.extend({
|
Whisper.MessageBodyTooLongToast = Whisper.ToastView.extend({
|
||||||
|
@ -588,9 +629,16 @@
|
||||||
this.$('.timeline-placeholder').append(this.timelineView.el);
|
this.$('.timeline-placeholder').append(this.timelineView.el);
|
||||||
},
|
},
|
||||||
|
|
||||||
showToast(ToastView) {
|
showToast(ToastView, options) {
|
||||||
const toast = new ToastView();
|
const toast = new ToastView(options);
|
||||||
toast.$el.appendTo(this.$el);
|
|
||||||
|
const lightboxEl = $('.module-lightbox');
|
||||||
|
if (lightboxEl.length > 0) {
|
||||||
|
toast.$el.appendTo(lightboxEl);
|
||||||
|
} else {
|
||||||
|
toast.$el.appendTo(this.$el);
|
||||||
|
}
|
||||||
|
|
||||||
toast.render();
|
toast.render();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1726,12 +1774,13 @@
|
||||||
|
|
||||||
const saveAttachment = async ({ attachment, message } = {}) => {
|
const saveAttachment = async ({ attachment, message } = {}) => {
|
||||||
const timestamp = message.sent_at;
|
const timestamp = message.sent_at;
|
||||||
Signal.Types.Attachment.save({
|
const name = await Signal.Types.Attachment.save({
|
||||||
attachment,
|
attachment,
|
||||||
document,
|
readAttachmentData,
|
||||||
getAbsolutePath: getAbsoluteAttachmentPath,
|
writeToDownloads,
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
});
|
||||||
|
this.showToast(Whisper.FileSavedToast, { name });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onItemClick = async ({ message, attachment, type }) => {
|
const onItemClick = async ({ message, attachment, type }) => {
|
||||||
|
@ -1916,18 +1965,19 @@
|
||||||
this.downloadAttachment({ attachment, timestamp, isDangerous });
|
this.downloadAttachment({ attachment, timestamp, isDangerous });
|
||||||
},
|
},
|
||||||
|
|
||||||
downloadAttachment({ attachment, timestamp, isDangerous }) {
|
async downloadAttachment({ attachment, timestamp, isDangerous }) {
|
||||||
if (isDangerous) {
|
if (isDangerous) {
|
||||||
this.showToast(Whisper.DangerousFileTypeToast);
|
this.showToast(Whisper.DangerousFileTypeToast);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Signal.Types.Attachment.save({
|
const name = await Signal.Types.Attachment.save({
|
||||||
attachment,
|
attachment,
|
||||||
document,
|
readAttachmentData,
|
||||||
getAbsolutePath: getAbsoluteAttachmentPath,
|
writeToDownloads,
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
});
|
||||||
|
this.showToast(Whisper.FileSavedToast, { name });
|
||||||
},
|
},
|
||||||
|
|
||||||
async displayTapToViewMessage(messageId) {
|
async displayTapToViewMessage(messageId) {
|
||||||
|
@ -2124,13 +2174,14 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSave = async (options = {}) => {
|
const onSave = async (options = {}) => {
|
||||||
Signal.Types.Attachment.save({
|
const name = await Signal.Types.Attachment.save({
|
||||||
attachment: options.attachment,
|
attachment: options.attachment,
|
||||||
document,
|
|
||||||
index: options.index + 1,
|
index: options.index + 1,
|
||||||
getAbsolutePath: getAbsoluteAttachmentPath,
|
readAttachmentData,
|
||||||
|
writeToDownloads,
|
||||||
timestamp: options.message.get('sent_at'),
|
timestamp: options.message.get('sent_at'),
|
||||||
});
|
});
|
||||||
|
this.showToast(Whisper.FileSavedToast, { name });
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
templateName: 'toast',
|
templateName: 'toast',
|
||||||
initialize() {
|
initialize() {
|
||||||
this.$el.hide();
|
this.$el.hide();
|
||||||
|
this.timeout = 2000;
|
||||||
},
|
},
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
|
@ -24,8 +25,9 @@
|
||||||
_.result(this, 'render_attributes', '')
|
_.result(this, 'render_attributes', '')
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
this.$el.attr('tabIndex', 0);
|
||||||
this.$el.show();
|
this.$el.show();
|
||||||
setTimeout(this.close.bind(this), 2000);
|
setTimeout(this.close.bind(this), this.timeout);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
"dev:typed-scss": "yarn build:typed-scss -w",
|
"dev:typed-scss": "yarn build:typed-scss -w",
|
||||||
"dev:storybook": "start-storybook -p 6006 -s ./",
|
"dev:storybook": "start-storybook -p 6006 -s ./",
|
||||||
"build": "run-s --print-label build:grunt build:typed-scss build:webpack build:release",
|
"build": "run-s --print-label build:grunt build:typed-scss build:webpack build:release",
|
||||||
|
"build:dev": "run-s --print-label build:grunt build:typed-scss build:webpack",
|
||||||
"build:grunt": "yarn grunt",
|
"build:grunt": "yarn grunt",
|
||||||
"build:typed-scss": "tsm sticker-creator",
|
"build:typed-scss": "tsm sticker-creator",
|
||||||
"build:webpack": "cross-env NODE_ENV=production webpack",
|
"build:webpack": "cross-env NODE_ENV=production webpack",
|
||||||
|
@ -56,6 +57,9 @@
|
||||||
"verify": "run-p --print-label verify:*",
|
"verify": "run-p --print-label verify:*",
|
||||||
"verify:ts": "tsc --noEmit"
|
"verify:ts": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fs-xattr": "0.3.0"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#00fd0f8a6623c6683280976d2a92b41d09c744bc",
|
"@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#00fd0f8a6623c6683280976d2a92b41d09c744bc",
|
||||||
"@sindresorhus/is": "0.8.0",
|
"@sindresorhus/is": "0.8.0",
|
||||||
|
@ -124,6 +128,7 @@
|
||||||
"reselect": "4.0.0",
|
"reselect": "4.0.0",
|
||||||
"rimraf": "2.6.2",
|
"rimraf": "2.6.2",
|
||||||
"sanitize.css": "11.0.0",
|
"sanitize.css": "11.0.0",
|
||||||
|
"sanitize-filename": "1.6.3",
|
||||||
"semver": "5.4.1",
|
"semver": "5.4.1",
|
||||||
"sharp": "0.23.0",
|
"sharp": "0.23.0",
|
||||||
"spellchecker": "3.7.0",
|
"spellchecker": "3.7.0",
|
||||||
|
|
|
@ -380,6 +380,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toast-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.confirmation-dialog {
|
.confirmation-dialog {
|
||||||
.content {
|
.content {
|
||||||
max-width: 350px;
|
max-width: 350px;
|
||||||
|
|
|
@ -3,8 +3,6 @@ import moment from 'moment';
|
||||||
import { isNumber, padStart } from 'lodash';
|
import { isNumber, padStart } from 'lodash';
|
||||||
|
|
||||||
import * as MIME from './MIME';
|
import * as MIME from './MIME';
|
||||||
import { arrayBufferToObjectURL } from '../util/arrayBufferToObjectURL';
|
|
||||||
import { saveURLAsFile } from '../util/saveURLAsFile';
|
|
||||||
import { SignalService } from '../protobuf';
|
import { SignalService } from '../protobuf';
|
||||||
import {
|
import {
|
||||||
isImageTypeSupported,
|
isImageTypeSupported,
|
||||||
|
@ -326,31 +324,37 @@ export const isVoiceMessage = (attachment: Attachment): boolean => {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const save = ({
|
export const save = async ({
|
||||||
attachment,
|
attachment,
|
||||||
document,
|
|
||||||
index,
|
index,
|
||||||
getAbsolutePath,
|
readAttachmentData,
|
||||||
|
writeToDownloads,
|
||||||
timestamp,
|
timestamp,
|
||||||
}: {
|
}: {
|
||||||
attachment: Attachment;
|
attachment: Attachment;
|
||||||
document: Document;
|
|
||||||
index: number;
|
index: number;
|
||||||
getAbsolutePath: (relativePath: string) => string;
|
readAttachmentData: (relativePath: string) => Promise<ArrayBuffer>;
|
||||||
|
writeToDownloads: (options: {
|
||||||
|
data: ArrayBuffer;
|
||||||
|
name: string;
|
||||||
|
}) => Promise<{ name: string; fullPath: string }>;
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
}): void => {
|
}): Promise<string> => {
|
||||||
const isObjectURLRequired = is.undefined(attachment.path);
|
if (!attachment.path && !attachment.data) {
|
||||||
const url = !is.undefined(attachment.path)
|
throw new Error('Attachment had neither path nor data');
|
||||||
? getAbsolutePath(attachment.path)
|
|
||||||
: arrayBufferToObjectURL({
|
|
||||||
data: attachment.data,
|
|
||||||
type: MIME.APPLICATION_OCTET_STREAM,
|
|
||||||
});
|
|
||||||
const filename = getSuggestedFilename({ attachment, timestamp, index });
|
|
||||||
saveURLAsFile({ url, filename, document });
|
|
||||||
if (isObjectURLRequired) {
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = attachment.path
|
||||||
|
? await readAttachmentData(attachment.path)
|
||||||
|
: attachment.data;
|
||||||
|
const name = getSuggestedFilename({ attachment, timestamp, index });
|
||||||
|
|
||||||
|
const { name: savedFilename } = await writeToDownloads({
|
||||||
|
data,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
|
||||||
|
return savedFilename;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSuggestedFilename = ({
|
export const getSuggestedFilename = ({
|
||||||
|
|
|
@ -1135,7 +1135,7 @@
|
||||||
"rule": "jQuery-html(",
|
"rule": "jQuery-html(",
|
||||||
"path": "js/views/toast_view.js",
|
"path": "js/views/toast_view.js",
|
||||||
"line": " this.$el.html(",
|
"line": " this.$el.html(",
|
||||||
"lineNumber": 21,
|
"lineNumber": 22,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-15T00:38:04.183Z"
|
"updated": "2018-09-15T00:38:04.183Z"
|
||||||
},
|
},
|
||||||
|
@ -1143,7 +1143,7 @@
|
||||||
"rule": "jQuery-appendTo(",
|
"rule": "jQuery-appendTo(",
|
||||||
"path": "js/views/toast_view.js",
|
"path": "js/views/toast_view.js",
|
||||||
"line": " toast.$el.appendTo(el);",
|
"line": " toast.$el.appendTo(el);",
|
||||||
"lineNumber": 34,
|
"lineNumber": 36,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-11-06T19:56:38.557Z",
|
"updated": "2019-11-06T19:56:38.557Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
/**
|
|
||||||
* @prettier
|
|
||||||
*/
|
|
||||||
export const saveURLAsFile = ({
|
|
||||||
filename,
|
|
||||||
url,
|
|
||||||
document,
|
|
||||||
}: {
|
|
||||||
filename: string;
|
|
||||||
url: string;
|
|
||||||
document: Document;
|
|
||||||
}): void => {
|
|
||||||
const anchorElement = document.createElement('a');
|
|
||||||
anchorElement.href = url;
|
|
||||||
anchorElement.download = filename;
|
|
||||||
anchorElement.click();
|
|
||||||
};
|
|
12
yarn.lock
12
yarn.lock
|
@ -7396,6 +7396,11 @@ fs-write-stream-atomic@^1.0.8:
|
||||||
imurmurhash "^0.1.4"
|
imurmurhash "^0.1.4"
|
||||||
readable-stream "1 || 2"
|
readable-stream "1 || 2"
|
||||||
|
|
||||||
|
fs-xattr@0.3.0:
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/fs-xattr/-/fs-xattr-0.3.0.tgz#019642eacc49f343061af19de4c13543895589ad"
|
||||||
|
integrity sha512-BixjoRM9etRFyWOtJRcflfu5HqBWLGTYbeHiL196VRUcc/nYgS2px6w4yVaj3XmrN1bk4rZBH82A8u5Z64YcXQ==
|
||||||
|
|
||||||
fs.realpath@^1.0.0:
|
fs.realpath@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||||
|
@ -14328,6 +14333,13 @@ samsam@1.3.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50"
|
resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50"
|
||||||
|
|
||||||
|
sanitize-filename@1.6.3:
|
||||||
|
version "1.6.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378"
|
||||||
|
integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==
|
||||||
|
dependencies:
|
||||||
|
truncate-utf8-bytes "^1.0.0"
|
||||||
|
|
||||||
sanitize-filename@^1.6.2:
|
sanitize-filename@^1.6.2:
|
||||||
version "1.6.2"
|
version "1.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.2.tgz#01b4fc8809f14e9d22761fe70380fe7f3f902185"
|
resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.2.tgz#01b4fc8809f14e9d22761fe70380fe7f3f902185"
|
||||||
|
|
Loading…
Add table
Reference in a new issue