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:
Scott Nonnenberg 2020-01-09 11:57:43 -08:00 committed by Ken Powers
parent 65befde0fa
commit 1bf9ca7233
11 changed files with 202 additions and 52 deletions

View file

@ -1,11 +1,22 @@
const crypto = require('crypto');
const path = require('path');
const { app, shell, remote } = require('electron');
const pify = require('pify');
const glob = require('glob');
const fse = require('fs-extra');
const toArrayBuffer = require('to-arraybuffer');
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 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 ->
// ArrayBuffer ->
// IO (Promise RelativePath)