Get rid of electron.remote
This commit is contained in:
parent
246583d274
commit
76d8b5e375
16 changed files with 563 additions and 591 deletions
|
@ -3299,30 +3299,6 @@ Signal Desktop makes use of the following open source projects.
|
|||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
## tmp
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 KARASZI István
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## typeface-inter
|
||||
|
||||
Copyright (c) 2016-2018 The Inter Project Authors (me@rsms.me)
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
getStickersPath,
|
||||
getTempPath,
|
||||
getDraftPath,
|
||||
} from './attachments';
|
||||
} from '../ts/util/attachments';
|
||||
|
||||
let initialized = false;
|
||||
|
||||
|
|
|
@ -1,43 +1,24 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { randomBytes } from 'crypto';
|
||||
import { basename, join, normalize, relative } from 'path';
|
||||
import { app, dialog, shell, remote } from 'electron';
|
||||
import { join, relative } from 'path';
|
||||
|
||||
import fastGlob from 'fast-glob';
|
||||
import glob from 'glob';
|
||||
import pify from 'pify';
|
||||
import fse from 'fs-extra';
|
||||
import { map, isTypedArray, isString } from 'lodash';
|
||||
import { map } from 'lodash';
|
||||
import normalizePath from 'normalize-path';
|
||||
import getGuid from 'uuid/v4';
|
||||
|
||||
import { isPathInside } from '../ts/util/isPathInside';
|
||||
import { isWindows } from '../ts/OS';
|
||||
import { writeWindowsZoneIdentifier } from '../ts/util/windowsZoneIdentifier';
|
||||
import {
|
||||
getPath,
|
||||
getStickersPath,
|
||||
getDraftPath,
|
||||
getTempPath,
|
||||
createDeleter,
|
||||
} from '../ts/util/attachments';
|
||||
|
||||
type FSAttrType = {
|
||||
set: (path: string, attribute: string, value: string) => Promise<void>;
|
||||
};
|
||||
|
||||
let xattr: FSAttrType | undefined;
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line max-len
|
||||
// eslint-disable-next-line global-require, import/no-extraneous-dependencies, import/no-unresolved
|
||||
xattr = require('fs-xattr');
|
||||
} catch (e) {
|
||||
console.log('x-attr dependency did not load successfully');
|
||||
}
|
||||
|
||||
const PATH = 'attachments.noindex';
|
||||
const AVATAR_PATH = 'avatars.noindex';
|
||||
const STICKER_PATH = 'stickers.noindex';
|
||||
const TEMP_PATH = 'temp';
|
||||
const DRAFT_PATH = 'drafts.noindex';
|
||||
|
||||
const getApp = () => app || remote.app;
|
||||
export * from '../ts/util/attachments';
|
||||
|
||||
export const getAllAttachments = async (
|
||||
userDataPath: string
|
||||
|
@ -79,274 +60,11 @@ export const getBuiltInImages = async (): Promise<ReadonlyArray<string>> => {
|
|||
return map(files, file => relative(dir, file));
|
||||
};
|
||||
|
||||
const createPathGetter = (subpath: string) => (
|
||||
userDataPath: string
|
||||
): string => {
|
||||
if (!isString(userDataPath)) {
|
||||
throw new TypeError("'userDataPath' must be a string");
|
||||
}
|
||||
return join(userDataPath, subpath);
|
||||
};
|
||||
|
||||
export const getAvatarsPath = createPathGetter(AVATAR_PATH);
|
||||
export const getDraftPath = createPathGetter(DRAFT_PATH);
|
||||
export const getPath = createPathGetter(PATH);
|
||||
export const getStickersPath = createPathGetter(STICKER_PATH);
|
||||
export const getTempPath = createPathGetter(TEMP_PATH);
|
||||
|
||||
export const clearTempPath = (userDataPath: string): Promise<void> => {
|
||||
const tempPath = getTempPath(userDataPath);
|
||||
return fse.emptyDir(tempPath);
|
||||
};
|
||||
|
||||
export const createReader = (
|
||||
root: string
|
||||
): ((relativePath: string) => Promise<Uint8Array>) => {
|
||||
if (!isString(root)) {
|
||||
throw new TypeError("'root' must be a path");
|
||||
}
|
||||
|
||||
return async (relativePath: string): Promise<Uint8Array> => {
|
||||
if (!isString(relativePath)) {
|
||||
throw new TypeError("'relativePath' must be a string");
|
||||
}
|
||||
|
||||
const absolutePath = join(root, relativePath);
|
||||
const normalized = normalize(absolutePath);
|
||||
if (!isPathInside(normalized, root)) {
|
||||
throw new Error('Invalid relative path');
|
||||
}
|
||||
return fse.readFile(normalized);
|
||||
};
|
||||
};
|
||||
|
||||
export const createDoesExist = (
|
||||
root: string
|
||||
): ((relativePath: string) => Promise<boolean>) => {
|
||||
if (!isString(root)) {
|
||||
throw new TypeError("'root' must be a path");
|
||||
}
|
||||
|
||||
return async (relativePath: string): Promise<boolean> => {
|
||||
if (!isString(relativePath)) {
|
||||
throw new TypeError("'relativePath' must be a string");
|
||||
}
|
||||
|
||||
const absolutePath = join(root, relativePath);
|
||||
const normalized = normalize(absolutePath);
|
||||
if (!isPathInside(normalized, root)) {
|
||||
throw new Error('Invalid relative path');
|
||||
}
|
||||
try {
|
||||
await fse.access(normalized, fse.constants.F_OK);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const copyIntoAttachmentsDirectory = (
|
||||
root: string
|
||||
): ((sourcePath: string) => Promise<{ path: string; size: number }>) => {
|
||||
if (!isString(root)) {
|
||||
throw new TypeError("'root' must be a path");
|
||||
}
|
||||
|
||||
const userDataPath = getApp().getPath('userData');
|
||||
|
||||
return async (
|
||||
sourcePath: string
|
||||
): Promise<{ path: string; size: number }> => {
|
||||
if (!isString(sourcePath)) {
|
||||
throw new TypeError('sourcePath must be a string');
|
||||
}
|
||||
|
||||
if (!isPathInside(sourcePath, userDataPath)) {
|
||||
throw new Error(
|
||||
"'sourcePath' must be relative to the user config directory"
|
||||
);
|
||||
}
|
||||
|
||||
const name = createName();
|
||||
const relativePath = getRelativePath(name);
|
||||
const absolutePath = join(root, relativePath);
|
||||
const normalized = normalize(absolutePath);
|
||||
if (!isPathInside(normalized, root)) {
|
||||
throw new Error('Invalid relative path');
|
||||
}
|
||||
|
||||
await fse.ensureFile(normalized);
|
||||
await fse.copy(sourcePath, normalized);
|
||||
const { size } = await fse.stat(normalized);
|
||||
|
||||
return {
|
||||
path: relativePath,
|
||||
size,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
async function writeWithAttributes(
|
||||
target: string,
|
||||
data: Uint8Array
|
||||
): Promise<void> {
|
||||
await fse.writeFile(target, 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(target, 'com.apple.quarantine', attrValue);
|
||||
} else if (isWindows()) {
|
||||
// This operation may fail (see the function's comments), which is not a show-stopper.
|
||||
try {
|
||||
await writeWindowsZoneIdentifier(target);
|
||||
} catch (err) {
|
||||
console.warn('Failed to write Windows Zone.Identifier file; continuing');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const openFileInDownloads = async (name: string): Promise<void> => {
|
||||
const shellToUse = shell || remote.shell;
|
||||
const appToUse = getApp();
|
||||
|
||||
const downloadsPath =
|
||||
appToUse.getPath('downloads') || appToUse.getPath('home');
|
||||
const target = join(downloadsPath, name);
|
||||
|
||||
const normalized = normalize(target);
|
||||
if (!isPathInside(normalized, downloadsPath)) {
|
||||
throw new Error('Invalid filename!');
|
||||
}
|
||||
|
||||
shellToUse.showItemInFolder(normalized);
|
||||
};
|
||||
|
||||
export const saveAttachmentToDisk = async ({
|
||||
data,
|
||||
name,
|
||||
}: {
|
||||
data: Uint8Array;
|
||||
name: string;
|
||||
}): Promise<null | { fullPath: string; name: string }> => {
|
||||
const dialogToUse = dialog || remote.dialog;
|
||||
const browserWindow = remote.getCurrentWindow();
|
||||
|
||||
const { canceled, filePath } = await dialogToUse.showSaveDialog(
|
||||
browserWindow,
|
||||
{
|
||||
defaultPath: name,
|
||||
}
|
||||
);
|
||||
|
||||
if (canceled || !filePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await writeWithAttributes(filePath, data);
|
||||
|
||||
const fileBasename = basename(filePath);
|
||||
|
||||
return {
|
||||
fullPath: filePath,
|
||||
name: fileBasename,
|
||||
};
|
||||
};
|
||||
|
||||
export const openFileInFolder = async (target: string): Promise<void> => {
|
||||
const shellToUse = shell || remote.shell;
|
||||
|
||||
shellToUse.showItemInFolder(target);
|
||||
};
|
||||
|
||||
export const createWriterForNew = (
|
||||
root: string
|
||||
): ((bytes: Uint8Array) => Promise<string>) => {
|
||||
if (!isString(root)) {
|
||||
throw new TypeError("'root' must be a path");
|
||||
}
|
||||
|
||||
return async (bytes: Uint8Array) => {
|
||||
if (!isTypedArray(bytes)) {
|
||||
throw new TypeError("'bytes' must be a typed array");
|
||||
}
|
||||
|
||||
const name = createName();
|
||||
const relativePath = getRelativePath(name);
|
||||
return createWriterForExisting(root)({
|
||||
data: bytes,
|
||||
path: relativePath,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const createWriterForExisting = (
|
||||
root: string
|
||||
): ((options: { data: Uint8Array; path: string }) => Promise<string>) => {
|
||||
if (!isString(root)) {
|
||||
throw new TypeError("'root' must be a path");
|
||||
}
|
||||
|
||||
return async ({
|
||||
data: bytes,
|
||||
path: relativePath,
|
||||
}: {
|
||||
data: Uint8Array;
|
||||
path: string;
|
||||
}): Promise<string> => {
|
||||
if (!isString(relativePath)) {
|
||||
throw new TypeError("'relativePath' must be a path");
|
||||
}
|
||||
|
||||
if (!isTypedArray(bytes)) {
|
||||
throw new TypeError("'arrayBuffer' must be an array buffer");
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(bytes);
|
||||
const absolutePath = join(root, relativePath);
|
||||
const normalized = normalize(absolutePath);
|
||||
if (!isPathInside(normalized, root)) {
|
||||
throw new Error('Invalid relative path');
|
||||
}
|
||||
|
||||
await fse.ensureFile(normalized);
|
||||
await fse.writeFile(normalized, buffer);
|
||||
return relativePath;
|
||||
};
|
||||
};
|
||||
|
||||
export const createDeleter = (
|
||||
root: string
|
||||
): ((relativePath: string) => Promise<void>) => {
|
||||
if (!isString(root)) {
|
||||
throw new TypeError("'root' must be a path");
|
||||
}
|
||||
|
||||
return async (relativePath: string): Promise<void> => {
|
||||
if (!isString(relativePath)) {
|
||||
throw new TypeError("'relativePath' must be a string");
|
||||
}
|
||||
|
||||
const absolutePath = join(root, relativePath);
|
||||
const normalized = normalize(absolutePath);
|
||||
if (!isPathInside(normalized, root)) {
|
||||
throw new Error('Invalid relative path');
|
||||
}
|
||||
await fse.remove(absolutePath);
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteAll = async ({
|
||||
userDataPath,
|
||||
attachments,
|
||||
|
@ -400,28 +118,3 @@ export const deleteAllDraftAttachments = async ({
|
|||
|
||||
console.log(`deleteAllDraftAttachments: deleted ${attachments.length} files`);
|
||||
};
|
||||
|
||||
export const createName = (): string => {
|
||||
const buffer = randomBytes(32);
|
||||
return buffer.toString('hex');
|
||||
};
|
||||
|
||||
export const getRelativePath = (name: string): string => {
|
||||
if (!isString(name)) {
|
||||
throw new TypeError("'name' must be a string");
|
||||
}
|
||||
|
||||
const prefix = name.slice(0, 2);
|
||||
return join(prefix, name);
|
||||
};
|
||||
|
||||
export const createAbsolutePathGetter = (rootPath: string) => (
|
||||
relativePath: string
|
||||
): string => {
|
||||
const absolutePath = join(rootPath, relativePath);
|
||||
const normalized = normalize(absolutePath);
|
||||
if (!isPathInside(normalized, rootPath)) {
|
||||
throw new Error('Invalid relative path');
|
||||
}
|
||||
return normalized;
|
||||
};
|
||||
|
|
31
app/main.ts
31
app/main.ts
|
@ -310,6 +310,9 @@ function prepareUrl(
|
|||
serverPublicParams: config.get<string>('serverPublicParams'),
|
||||
serverTrustRoot: config.get<string>('serverTrustRoot'),
|
||||
appStartInitialSpellcheckSetting,
|
||||
userDataPath: app.getPath('userData'),
|
||||
downloadsPath: app.getPath('downloads'),
|
||||
homePath: app.getPath('home'),
|
||||
...moreKeys,
|
||||
}).href;
|
||||
}
|
||||
|
@ -2063,3 +2066,31 @@ async function ensureFilePermissions(onlyFiles?: Array<string>) {
|
|||
|
||||
getLogger().info(`Finish ensuring permissions in ${Date.now() - start}ms`);
|
||||
}
|
||||
|
||||
ipc.handle('get-auto-launch', async () => {
|
||||
return app.getLoginItemSettings().openAtLogin;
|
||||
});
|
||||
|
||||
ipc.handle('set-auto-launch', async (_event, value) => {
|
||||
app.setLoginItemSettings({ openAtLogin: Boolean(value) });
|
||||
});
|
||||
|
||||
ipc.on('show-message-box', (_event, { type, message }) => {
|
||||
dialog.showMessageBox({ type, message });
|
||||
});
|
||||
|
||||
ipc.on('show-item-in-folder', (_event, folder) => {
|
||||
shell.showItemInFolder(folder);
|
||||
});
|
||||
|
||||
ipc.handle('show-save-dialog', async (_event, { defaultPath }) => {
|
||||
if (!mainWindow) {
|
||||
getLogger().warn('show-save-dialog: no main window');
|
||||
|
||||
return { canceled: true };
|
||||
}
|
||||
|
||||
return dialog.showSaveDialog(mainWindow, {
|
||||
defaultPath,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@ const {
|
|||
sample,
|
||||
} = require('lodash');
|
||||
|
||||
const Attachments = require('../../app/attachments');
|
||||
const Attachments = require('../../ts/windows/attachments');
|
||||
const Message = require('./types/message');
|
||||
const { sleep } = require('../../ts/util/sleep');
|
||||
|
||||
|
|
|
@ -161,7 +161,6 @@
|
|||
"sharp": "0.28.1",
|
||||
"split2": "4.0.0",
|
||||
"testcheck": "1.0.0-rc.2",
|
||||
"tmp": "0.0.33",
|
||||
"typeface-inter": "3.10.0",
|
||||
"underscore": "1.12.1",
|
||||
"uuid": "3.3.2",
|
||||
|
|
13
preload.js
13
preload.js
|
@ -21,9 +21,6 @@ try {
|
|||
const { getEnvironment, Environment } = require('./ts/environment');
|
||||
const ipc = electron.ipcRenderer;
|
||||
|
||||
const { remote } = electron;
|
||||
const { app } = remote;
|
||||
|
||||
const config = require('url').parse(window.location.toString(), true).query;
|
||||
|
||||
const log = require('./ts/logging/log');
|
||||
|
@ -72,9 +69,11 @@ try {
|
|||
window.getServerPublicParams = () => config.serverPublicParams;
|
||||
window.getSfuUrl = () => config.sfuUrl;
|
||||
window.isBehindProxy = () => Boolean(config.proxyUrl);
|
||||
window.getAutoLaunch = () => app.getLoginItemSettings().openAtLogin;
|
||||
window.getAutoLaunch = () => {
|
||||
return ipc.invoke('get-auto-launch');
|
||||
};
|
||||
window.setAutoLaunch = value => {
|
||||
app.setLoginItemSettings({ openAtLogin: Boolean(value) });
|
||||
return ipc.invoke('set-auto-launch', value);
|
||||
};
|
||||
|
||||
window.isBeforeVersion = (toCheck, baseVersion) => {
|
||||
|
@ -405,7 +404,7 @@ try {
|
|||
window.PQueue = require('p-queue').default;
|
||||
|
||||
const Signal = require('./js/modules/signal');
|
||||
const Attachments = require('./app/attachments');
|
||||
const Attachments = require('./ts/windows/attachments');
|
||||
|
||||
const { locale } = config;
|
||||
window.i18n = SignalContext.i18n;
|
||||
|
@ -418,7 +417,7 @@ try {
|
|||
});
|
||||
window.moment.locale(locale);
|
||||
|
||||
const userDataPath = app.getPath('userData');
|
||||
const userDataPath = SignalContext.getPath('userData');
|
||||
window.baseAttachmentsPath = Attachments.getPath(userDataPath);
|
||||
window.baseStickersPath = Attachments.getStickersPath(userDataPath);
|
||||
window.baseTempPath = Attachments.getTempPath(userDataPath);
|
||||
|
|
|
@ -27,7 +27,6 @@ window.test = {
|
|||
fastGlob,
|
||||
normalizePath: require('normalize-path'),
|
||||
fse: require('fs-extra'),
|
||||
tmp: require('tmp'),
|
||||
path: require('path'),
|
||||
basePath: __dirname,
|
||||
attachmentsPath: window.Signal.Migrations.attachmentsPath,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* global window */
|
||||
const { ipcRenderer: ipc, remote } = require('electron');
|
||||
const { ipcRenderer: ipc } = require('electron');
|
||||
const sharp = require('sharp');
|
||||
const pify = require('pify');
|
||||
const { readFile } = require('fs');
|
||||
|
@ -23,8 +23,6 @@ const { SignalService: Proto } = require('../ts/protobuf');
|
|||
const { getEnvironment } = require('../ts/environment');
|
||||
const { createSetting } = require('../ts/util/preload');
|
||||
|
||||
const { dialog } = remote;
|
||||
|
||||
const STICKER_SIZE = 512;
|
||||
const MIN_STICKER_DIMENSION = 10;
|
||||
const MAX_STICKER_DIMENSION = STICKER_SIZE;
|
||||
|
@ -171,7 +169,7 @@ window.encryptAndUpload = async (
|
|||
'StickerCreator--Authentication--error'
|
||||
];
|
||||
|
||||
dialog.showMessageBox({
|
||||
ipc.send('show-message-box', {
|
||||
type: 'warning',
|
||||
message,
|
||||
});
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
const fs = require('fs');
|
||||
const fse = require('fs-extra');
|
||||
const path = require('path');
|
||||
const tmp = require('tmp');
|
||||
const { assert } = require('chai');
|
||||
const { app } = require('electron');
|
||||
|
||||
const Attachments = require('../../app/attachments');
|
||||
const Bytes = require('../../ts/Bytes');
|
||||
import { assert } from 'chai';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import fse from 'fs-extra';
|
||||
import * as Attachments from '../../windows/attachments';
|
||||
import * as Bytes from '../../Bytes';
|
||||
|
||||
const PREFIX_LENGTH = 2;
|
||||
const NUM_SEPARATORS = 1;
|
||||
|
@ -17,45 +15,128 @@ const NAME_LENGTH = 64;
|
|||
const PATH_LENGTH = PREFIX_LENGTH + NUM_SEPARATORS + NAME_LENGTH;
|
||||
|
||||
describe('Attachments', () => {
|
||||
describe('createWriterForNew', () => {
|
||||
let tempRootDirectory = null;
|
||||
const USER_DATA = window.SignalContext.getPath('userData');
|
||||
|
||||
let tempRootDirectory: string;
|
||||
|
||||
before(() => {
|
||||
tempRootDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'Signal'));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await fse.remove(tempRootDirectory);
|
||||
});
|
||||
|
||||
describe('createReader', () => {
|
||||
it('should read file from disk', async () => {
|
||||
const tempDirectory = path.join(
|
||||
tempRootDirectory,
|
||||
'Attachments_createReader'
|
||||
);
|
||||
|
||||
const relativePath = Attachments.getRelativePath(
|
||||
Attachments.createName()
|
||||
);
|
||||
const fullPath = path.join(tempDirectory, relativePath);
|
||||
const input = Bytes.fromString('test string');
|
||||
|
||||
const inputBuffer = Buffer.from(input);
|
||||
await fse.ensureFile(fullPath);
|
||||
await fse.writeFile(fullPath, inputBuffer);
|
||||
const output = await Attachments.createReader(tempDirectory)(
|
||||
relativePath
|
||||
);
|
||||
|
||||
assert.deepEqual(input, output);
|
||||
});
|
||||
|
||||
it('throws if relative path goes higher than root', async () => {
|
||||
const tempDirectory = path.join(
|
||||
tempRootDirectory,
|
||||
'Attachments_createReader'
|
||||
);
|
||||
|
||||
const relativePath = '../../parent';
|
||||
|
||||
await assert.isRejected(
|
||||
Attachments.createReader(tempDirectory)(relativePath),
|
||||
'Invalid relative path'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyIntoAttachmentsDirectory', () => {
|
||||
let filesToRemove: Array<string>;
|
||||
|
||||
const getFakeAttachmentsDirectory = () => {
|
||||
const result = path.join(
|
||||
USER_DATA,
|
||||
`fake-attachments-${Date.now()}-${Math.random()
|
||||
.toString()
|
||||
.substring(2)}`
|
||||
);
|
||||
filesToRemove.push(result);
|
||||
return result;
|
||||
};
|
||||
|
||||
// These tests use the `userData` path. In `electron-mocha`, these are temporary
|
||||
// directories; no need to be concerned about messing with the "real" directory.
|
||||
before(() => {
|
||||
tempRootDirectory = tmp.dirSync().name;
|
||||
filesToRemove = [];
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await fse.remove(tempRootDirectory);
|
||||
await Promise.all(filesToRemove.map(toRemove => fse.remove(toRemove)));
|
||||
filesToRemove = [];
|
||||
});
|
||||
|
||||
it('should write file to disk and return path', async () => {
|
||||
const input = Bytes.fromString('test string');
|
||||
const tempDirectory = path.join(
|
||||
tempRootDirectory,
|
||||
'Attachments_createWriterForNew'
|
||||
it('throws if passed a non-string', () => {
|
||||
assert.throws(() => {
|
||||
Attachments.copyIntoAttachmentsDirectory((1234 as unknown) as string);
|
||||
}, TypeError);
|
||||
assert.throws(() => {
|
||||
Attachments.copyIntoAttachmentsDirectory((null as unknown) as string);
|
||||
}, TypeError);
|
||||
});
|
||||
|
||||
it('returns a function that rejects if the source path is not a string', async () => {
|
||||
const copier = Attachments.copyIntoAttachmentsDirectory(
|
||||
await getFakeAttachmentsDirectory()
|
||||
);
|
||||
await assert.isRejected(copier((123 as unknown) as string));
|
||||
});
|
||||
|
||||
it('returns a function that rejects if the source path is not in the user config directory', async () => {
|
||||
const copier = Attachments.copyIntoAttachmentsDirectory(
|
||||
await getFakeAttachmentsDirectory()
|
||||
);
|
||||
await assert.isRejected(
|
||||
copier(path.join(tempRootDirectory, 'hello.txt')),
|
||||
"'sourcePath' must be relative to the user config directory"
|
||||
);
|
||||
});
|
||||
|
||||
it('returns a function that copies the source path into the attachments directory and returns its path and size', async () => {
|
||||
const attachmentsPath = await getFakeAttachmentsDirectory();
|
||||
const someOtherPath = path.join(USER_DATA, 'somethingElse');
|
||||
await fse.outputFile(someOtherPath, 'hello world');
|
||||
filesToRemove.push(someOtherPath);
|
||||
|
||||
const copier = Attachments.copyIntoAttachmentsDirectory(attachmentsPath);
|
||||
const { path: relativePath, size } = await copier(someOtherPath);
|
||||
|
||||
const absolutePath = path.join(attachmentsPath, relativePath);
|
||||
assert.notEqual(someOtherPath, absolutePath);
|
||||
assert.strictEqual(
|
||||
await fs.promises.readFile(absolutePath, 'utf8'),
|
||||
'hello world'
|
||||
);
|
||||
|
||||
const outputPath = await Attachments.createWriterForNew(tempDirectory)(
|
||||
input
|
||||
);
|
||||
const output = await fse.readFile(path.join(tempDirectory, outputPath));
|
||||
|
||||
assert.lengthOf(outputPath, PATH_LENGTH);
|
||||
|
||||
const inputBuffer = Buffer.from(input);
|
||||
assert.deepEqual(inputBuffer, output);
|
||||
assert.strictEqual(size, 'hello world'.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createWriterForExisting', () => {
|
||||
let tempRootDirectory = null;
|
||||
before(() => {
|
||||
tempRootDirectory = tmp.dirSync().name;
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await fse.remove(tempRootDirectory);
|
||||
});
|
||||
|
||||
it('should write file to disk on given path and return path', async () => {
|
||||
const input = Bytes.fromString('test string');
|
||||
const tempDirectory = path.join(
|
||||
|
@ -104,205 +185,23 @@ describe('Attachments', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('createReader', () => {
|
||||
let tempRootDirectory = null;
|
||||
before(() => {
|
||||
tempRootDirectory = tmp.dirSync().name;
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await fse.remove(tempRootDirectory);
|
||||
});
|
||||
|
||||
it('should read file from disk', async () => {
|
||||
describe('createWriterForNew', () => {
|
||||
it('should write file to disk and return path', async () => {
|
||||
const input = Bytes.fromString('test string');
|
||||
const tempDirectory = path.join(
|
||||
tempRootDirectory,
|
||||
'Attachments_createReader'
|
||||
'Attachments_createWriterForNew'
|
||||
);
|
||||
|
||||
const relativePath = Attachments.getRelativePath(
|
||||
Attachments.createName()
|
||||
const outputPath = await Attachments.createWriterForNew(tempDirectory)(
|
||||
input
|
||||
);
|
||||
const fullPath = path.join(tempDirectory, relativePath);
|
||||
const input = Bytes.fromString('test string');
|
||||
const output = await fse.readFile(path.join(tempDirectory, outputPath));
|
||||
|
||||
assert.lengthOf(outputPath, PATH_LENGTH);
|
||||
|
||||
const inputBuffer = Buffer.from(input);
|
||||
await fse.ensureFile(fullPath);
|
||||
await fse.writeFile(fullPath, inputBuffer);
|
||||
const output = await Attachments.createReader(tempDirectory)(
|
||||
relativePath
|
||||
);
|
||||
|
||||
assert.deepEqual(input, output);
|
||||
});
|
||||
|
||||
it('throws if relative path goes higher than root', async () => {
|
||||
const tempDirectory = path.join(
|
||||
tempRootDirectory,
|
||||
'Attachments_createReader'
|
||||
);
|
||||
|
||||
const relativePath = '../../parent';
|
||||
|
||||
try {
|
||||
await Attachments.createReader(tempDirectory)(relativePath);
|
||||
} catch (error) {
|
||||
assert.strictEqual(error.message, 'Invalid relative path');
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Expected an error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyIntoAttachmentsDirectory', () => {
|
||||
// These tests use the `userData` path. In `electron-mocha`, these are temporary
|
||||
// directories; no need to be concerned about messing with the "real" directory.
|
||||
before(function thisNeeded() {
|
||||
this.filesToRemove = [];
|
||||
this.getFakeAttachmentsDirectory = () => {
|
||||
const result = path.join(
|
||||
app.getPath('userData'),
|
||||
`fake-attachments-${Date.now()}-${Math.random()
|
||||
.toString()
|
||||
.substring(2)}`
|
||||
);
|
||||
this.filesToRemove.push(result);
|
||||
return result;
|
||||
};
|
||||
this.getTempFile = () => {
|
||||
const result = tmp.fileSync().name;
|
||||
this.filesToRemove.push(result);
|
||||
return result;
|
||||
};
|
||||
});
|
||||
|
||||
after(async function thisNeeded() {
|
||||
await Promise.all(
|
||||
this.filesToRemove.map(toRemove => fse.remove(toRemove))
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if passed a non-string', () => {
|
||||
assert.throws(() => {
|
||||
Attachments.copyIntoAttachmentsDirectory(1234);
|
||||
}, TypeError);
|
||||
assert.throws(() => {
|
||||
Attachments.copyIntoAttachmentsDirectory(null);
|
||||
}, TypeError);
|
||||
});
|
||||
|
||||
it('returns a function that rejects if the source path is not a string', async function thisNeeded() {
|
||||
const copier = Attachments.copyIntoAttachmentsDirectory(
|
||||
await this.getFakeAttachmentsDirectory()
|
||||
);
|
||||
return copier(123)
|
||||
.then(() => {
|
||||
assert.fail('This should never be run');
|
||||
})
|
||||
.catch(err => {
|
||||
assert.instanceOf(err, TypeError);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a function that rejects if the source path is not in the user config directory', async function thisNeeded() {
|
||||
const copier = Attachments.copyIntoAttachmentsDirectory(
|
||||
await this.getFakeAttachmentsDirectory()
|
||||
);
|
||||
return copier(this.getTempFile())
|
||||
.then(() => {
|
||||
assert.fail('This should never be run');
|
||||
})
|
||||
.catch(err => {
|
||||
assert.instanceOf(err, Error);
|
||||
assert.strictEqual(
|
||||
err.message,
|
||||
"'sourcePath' must be relative to the user config directory"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a function that copies the source path into the attachments directory and returns its path and size', async function thisNeeded() {
|
||||
const attachmentsPath = await this.getFakeAttachmentsDirectory();
|
||||
const someOtherPath = path.join(app.getPath('userData'), 'somethingElse');
|
||||
await fse.outputFile(someOtherPath, 'hello world');
|
||||
this.filesToRemove.push(someOtherPath);
|
||||
|
||||
const copier = Attachments.copyIntoAttachmentsDirectory(attachmentsPath);
|
||||
const { path: relativePath, size } = await copier(someOtherPath);
|
||||
|
||||
const absolutePath = path.join(attachmentsPath, relativePath);
|
||||
assert.notEqual(someOtherPath, absolutePath);
|
||||
assert.strictEqual(
|
||||
await fs.promises.readFile(absolutePath, 'utf8'),
|
||||
'hello world'
|
||||
);
|
||||
|
||||
assert.strictEqual(size, 'hello world'.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createDeleter', () => {
|
||||
let tempRootDirectory = null;
|
||||
before(() => {
|
||||
tempRootDirectory = tmp.dirSync().name;
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await fse.remove(tempRootDirectory);
|
||||
});
|
||||
|
||||
it('should delete file from disk', async () => {
|
||||
const tempDirectory = path.join(
|
||||
tempRootDirectory,
|
||||
'Attachments_createDeleter'
|
||||
);
|
||||
|
||||
const relativePath = Attachments.getRelativePath(
|
||||
Attachments.createName()
|
||||
);
|
||||
const fullPath = path.join(tempDirectory, relativePath);
|
||||
const input = Bytes.fromString('test string');
|
||||
|
||||
const inputBuffer = Buffer.from(input);
|
||||
await fse.ensureFile(fullPath);
|
||||
await fse.writeFile(fullPath, inputBuffer);
|
||||
await Attachments.createDeleter(tempDirectory)(relativePath);
|
||||
|
||||
const existsFile = await fse.exists(fullPath);
|
||||
assert.isFalse(existsFile);
|
||||
});
|
||||
|
||||
it('throws if relative path goes higher than root', async () => {
|
||||
const tempDirectory = path.join(
|
||||
tempRootDirectory,
|
||||
'Attachments_createDeleter'
|
||||
);
|
||||
|
||||
const relativePath = '../../parent';
|
||||
|
||||
try {
|
||||
await Attachments.createDeleter(tempDirectory)(relativePath);
|
||||
} catch (error) {
|
||||
assert.strictEqual(error.message, 'Invalid relative path');
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Expected an error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createName', () => {
|
||||
it('should return random file name with correct length', () => {
|
||||
assert.lengthOf(Attachments.createName(), NAME_LENGTH);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRelativePath', () => {
|
||||
it('should return correct path', () => {
|
||||
const name =
|
||||
'608ce3bc536edbf7637a6aeb6040bdfec49349140c0dd43e97c7ce263b15ff7e';
|
||||
assert.lengthOf(Attachments.getRelativePath(name), PATH_LENGTH);
|
||||
assert.deepEqual(inputBuffer, output);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -336,4 +235,59 @@ describe('Attachments', () => {
|
|||
throw new Error('Expected an error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createName', () => {
|
||||
it('should return random file name with correct length', () => {
|
||||
assert.lengthOf(Attachments.createName(), NAME_LENGTH);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRelativePath', () => {
|
||||
it('should return correct path', () => {
|
||||
const name =
|
||||
'608ce3bc536edbf7637a6aeb6040bdfec49349140c0dd43e97c7ce263b15ff7e';
|
||||
assert.lengthOf(Attachments.getRelativePath(name), PATH_LENGTH);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createDeleter', () => {
|
||||
it('should delete file from disk', async () => {
|
||||
const tempDirectory = path.join(
|
||||
tempRootDirectory,
|
||||
'Attachments_createDeleter'
|
||||
);
|
||||
|
||||
const relativePath = Attachments.getRelativePath(
|
||||
Attachments.createName()
|
||||
);
|
||||
const fullPath = path.join(tempDirectory, relativePath);
|
||||
const input = Bytes.fromString('test string');
|
||||
|
||||
const inputBuffer = Buffer.from(input);
|
||||
await fse.ensureFile(fullPath);
|
||||
await fse.writeFile(fullPath, inputBuffer);
|
||||
await Attachments.createDeleter(tempDirectory)(relativePath);
|
||||
|
||||
const existsFile = await fse.pathExists(fullPath);
|
||||
assert.isFalse(existsFile);
|
||||
});
|
||||
|
||||
it('throws if relative path goes higher than root', async () => {
|
||||
const tempDirectory = path.join(
|
||||
tempRootDirectory,
|
||||
'Attachments_createDeleter'
|
||||
);
|
||||
|
||||
const relativePath = '../../parent';
|
||||
|
||||
try {
|
||||
await Attachments.createDeleter(tempDirectory)(relativePath);
|
||||
} catch (error) {
|
||||
assert.strictEqual(error.message, 'Invalid relative path');
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Expected an error');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -26,7 +26,7 @@ import rimraf from 'rimraf';
|
|||
import type { BrowserWindow } from 'electron';
|
||||
import { app, ipcMain } from 'electron';
|
||||
|
||||
import { getTempPath } from '../../app/attachments';
|
||||
import { getTempPath } from '../util/attachments';
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
import { getUserAgent } from '../util/getUserAgent';
|
||||
import { isAlpha, isBeta } from '../util/version';
|
||||
|
|
50
ts/util/attachments.ts
Normal file
50
ts/util/attachments.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isString } from 'lodash';
|
||||
import { join, normalize } from 'path';
|
||||
import fse from 'fs-extra';
|
||||
|
||||
import { isPathInside } from './isPathInside';
|
||||
|
||||
const PATH = 'attachments.noindex';
|
||||
const AVATAR_PATH = 'avatars.noindex';
|
||||
const STICKER_PATH = 'stickers.noindex';
|
||||
const TEMP_PATH = 'temp';
|
||||
const DRAFT_PATH = 'drafts.noindex';
|
||||
|
||||
const createPathGetter = (subpath: string) => (
|
||||
userDataPath: string
|
||||
): string => {
|
||||
if (!isString(userDataPath)) {
|
||||
throw new TypeError("'userDataPath' must be a string");
|
||||
}
|
||||
return join(userDataPath, subpath);
|
||||
};
|
||||
|
||||
export const getAvatarsPath = createPathGetter(AVATAR_PATH);
|
||||
export const getDraftPath = createPathGetter(DRAFT_PATH);
|
||||
export const getPath = createPathGetter(PATH);
|
||||
export const getStickersPath = createPathGetter(STICKER_PATH);
|
||||
export const getTempPath = createPathGetter(TEMP_PATH);
|
||||
|
||||
export const createDeleter = (
|
||||
root: string
|
||||
): ((relativePath: string) => Promise<void>) => {
|
||||
if (!isString(root)) {
|
||||
throw new TypeError("'root' must be a path");
|
||||
}
|
||||
|
||||
return async (relativePath: string): Promise<void> => {
|
||||
if (!isString(relativePath)) {
|
||||
throw new TypeError("'relativePath' must be a string");
|
||||
}
|
||||
|
||||
const absolutePath = join(root, relativePath);
|
||||
const normalized = normalize(absolutePath);
|
||||
if (!isPathInside(normalized, root)) {
|
||||
throw new Error('Invalid relative path');
|
||||
}
|
||||
await fse.remove(absolutePath);
|
||||
};
|
||||
};
|
|
@ -114,7 +114,7 @@ export type IPCEventsCallbacksType = {
|
|||
type ValuesWithGetters = Omit<
|
||||
IPCEventsValuesType,
|
||||
// Optional
|
||||
'mediaPermissions' | 'mediaCameraPermissions'
|
||||
'mediaPermissions' | 'mediaCameraPermissions' | 'autoLaunch'
|
||||
>;
|
||||
|
||||
type ValuesWithSetters = Omit<
|
||||
|
@ -146,6 +146,7 @@ export type IPCEventsGettersType = {
|
|||
} & {
|
||||
getMediaPermissions?: () => Promise<boolean>;
|
||||
getMediaCameraPermissions?: () => Promise<boolean>;
|
||||
getAutoLaunch?: () => Promise<boolean>;
|
||||
};
|
||||
|
||||
export type IPCEventsSettersType = {
|
||||
|
@ -330,7 +331,7 @@ export function createIPCEvents(
|
|||
|
||||
getAutoLaunch: () => window.getAutoLaunch(),
|
||||
setAutoLaunch: async (value: boolean) => {
|
||||
window.setAutoLaunch(value);
|
||||
return window.setAutoLaunch(value);
|
||||
},
|
||||
|
||||
isPhoneNumberSharingEnabled: () => isPhoneNumberSharingEnabled(),
|
||||
|
|
4
ts/window.d.ts
vendored
4
ts/window.d.ts
vendored
|
@ -170,8 +170,8 @@ declare global {
|
|||
imageToBlurHash: typeof imageToBlurHash;
|
||||
loadImage: any;
|
||||
isBehindProxy: () => boolean;
|
||||
getAutoLaunch: () => boolean;
|
||||
setAutoLaunch: (value: boolean) => void;
|
||||
getAutoLaunch: () => Promise<boolean>;
|
||||
setAutoLaunch: (value: boolean) => Promise<void>;
|
||||
|
||||
PQueue: typeof PQueue;
|
||||
PQueueType: PQueue;
|
||||
|
|
268
ts/windows/attachments.ts
Normal file
268
ts/windows/attachments.ts
Normal file
|
@ -0,0 +1,268 @@
|
|||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { isString, isTypedArray } from 'lodash';
|
||||
import { join, normalize, basename } from 'path';
|
||||
import fse from 'fs-extra';
|
||||
import getGuid from 'uuid/v4';
|
||||
|
||||
import { getRandomBytes } from '../Crypto';
|
||||
import * as Bytes from '../Bytes';
|
||||
|
||||
import { isPathInside } from '../util/isPathInside';
|
||||
import { writeWindowsZoneIdentifier } from '../util/windowsZoneIdentifier';
|
||||
import { isWindows } from '../OS';
|
||||
|
||||
export * from '../util/attachments';
|
||||
|
||||
type FSAttrType = {
|
||||
set: (path: string, attribute: string, value: string) => Promise<void>;
|
||||
};
|
||||
|
||||
let xattr: FSAttrType | undefined;
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line max-len
|
||||
// eslint-disable-next-line global-require, import/no-extraneous-dependencies, import/no-unresolved
|
||||
xattr = require('fs-xattr');
|
||||
} catch (e) {
|
||||
window.SignalContext.log?.info('x-attr dependency did not load successfully');
|
||||
}
|
||||
|
||||
export const createReader = (
|
||||
root: string
|
||||
): ((relativePath: string) => Promise<Uint8Array>) => {
|
||||
if (!isString(root)) {
|
||||
throw new TypeError("'root' must be a path");
|
||||
}
|
||||
|
||||
return async (relativePath: string): Promise<Uint8Array> => {
|
||||
if (!isString(relativePath)) {
|
||||
throw new TypeError("'relativePath' must be a string");
|
||||
}
|
||||
|
||||
const absolutePath = join(root, relativePath);
|
||||
const normalized = normalize(absolutePath);
|
||||
if (!isPathInside(normalized, root)) {
|
||||
throw new Error('Invalid relative path');
|
||||
}
|
||||
return fse.readFile(normalized);
|
||||
};
|
||||
};
|
||||
|
||||
export const getRelativePath = (name: string): string => {
|
||||
if (!isString(name)) {
|
||||
throw new TypeError("'name' must be a string");
|
||||
}
|
||||
|
||||
const prefix = name.slice(0, 2);
|
||||
return join(prefix, name);
|
||||
};
|
||||
|
||||
export const createName = (): string => {
|
||||
const buffer = getRandomBytes(32);
|
||||
return Bytes.toHex(buffer);
|
||||
};
|
||||
|
||||
export const copyIntoAttachmentsDirectory = (
|
||||
root: string
|
||||
): ((sourcePath: string) => Promise<{ path: string; size: number }>) => {
|
||||
if (!isString(root)) {
|
||||
throw new TypeError("'root' must be a path");
|
||||
}
|
||||
|
||||
const userDataPath = window.SignalContext.getPath('userData');
|
||||
|
||||
return async (
|
||||
sourcePath: string
|
||||
): Promise<{ path: string; size: number }> => {
|
||||
if (!isString(sourcePath)) {
|
||||
throw new TypeError('sourcePath must be a string');
|
||||
}
|
||||
|
||||
if (!isPathInside(sourcePath, userDataPath)) {
|
||||
throw new Error(
|
||||
"'sourcePath' must be relative to the user config directory"
|
||||
);
|
||||
}
|
||||
|
||||
const name = createName();
|
||||
const relativePath = getRelativePath(name);
|
||||
const absolutePath = join(root, relativePath);
|
||||
const normalized = normalize(absolutePath);
|
||||
if (!isPathInside(normalized, root)) {
|
||||
throw new Error('Invalid relative path');
|
||||
}
|
||||
|
||||
await fse.ensureFile(normalized);
|
||||
await fse.copy(sourcePath, normalized);
|
||||
const { size } = await fse.stat(normalized);
|
||||
|
||||
return {
|
||||
path: relativePath,
|
||||
size,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const createWriterForNew = (
|
||||
root: string
|
||||
): ((bytes: Uint8Array) => Promise<string>) => {
|
||||
if (!isString(root)) {
|
||||
throw new TypeError("'root' must be a path");
|
||||
}
|
||||
|
||||
return async (bytes: Uint8Array) => {
|
||||
if (!isTypedArray(bytes)) {
|
||||
throw new TypeError("'bytes' must be a typed array");
|
||||
}
|
||||
|
||||
const name = createName();
|
||||
const relativePath = getRelativePath(name);
|
||||
return createWriterForExisting(root)({
|
||||
data: bytes,
|
||||
path: relativePath,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const createWriterForExisting = (
|
||||
root: string
|
||||
): ((options: { data: Uint8Array; path: string }) => Promise<string>) => {
|
||||
if (!isString(root)) {
|
||||
throw new TypeError("'root' must be a path");
|
||||
}
|
||||
|
||||
return async ({
|
||||
data: bytes,
|
||||
path: relativePath,
|
||||
}: {
|
||||
data: Uint8Array;
|
||||
path: string;
|
||||
}): Promise<string> => {
|
||||
if (!isString(relativePath)) {
|
||||
throw new TypeError("'relativePath' must be a path");
|
||||
}
|
||||
|
||||
if (!isTypedArray(bytes)) {
|
||||
throw new TypeError("'arrayBuffer' must be an array buffer");
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(bytes);
|
||||
const absolutePath = join(root, relativePath);
|
||||
const normalized = normalize(absolutePath);
|
||||
if (!isPathInside(normalized, root)) {
|
||||
throw new Error('Invalid relative path');
|
||||
}
|
||||
|
||||
await fse.ensureFile(normalized);
|
||||
await fse.writeFile(normalized, buffer);
|
||||
return relativePath;
|
||||
};
|
||||
};
|
||||
|
||||
export const createAbsolutePathGetter = (rootPath: string) => (
|
||||
relativePath: string
|
||||
): string => {
|
||||
const absolutePath = join(rootPath, relativePath);
|
||||
const normalized = normalize(absolutePath);
|
||||
if (!isPathInside(normalized, rootPath)) {
|
||||
throw new Error('Invalid relative path');
|
||||
}
|
||||
return normalized;
|
||||
};
|
||||
|
||||
export const createDoesExist = (
|
||||
root: string
|
||||
): ((relativePath: string) => Promise<boolean>) => {
|
||||
if (!isString(root)) {
|
||||
throw new TypeError("'root' must be a path");
|
||||
}
|
||||
|
||||
return async (relativePath: string): Promise<boolean> => {
|
||||
if (!isString(relativePath)) {
|
||||
throw new TypeError("'relativePath' must be a string");
|
||||
}
|
||||
|
||||
const absolutePath = join(root, relativePath);
|
||||
const normalized = normalize(absolutePath);
|
||||
if (!isPathInside(normalized, root)) {
|
||||
throw new Error('Invalid relative path');
|
||||
}
|
||||
try {
|
||||
await fse.access(normalized, fse.constants.F_OK);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const openFileInFolder = async (target: string): Promise<void> => {
|
||||
ipcRenderer.send('show-item-in-folder', target);
|
||||
};
|
||||
|
||||
const showSaveDialog = (
|
||||
defaultPath: string
|
||||
): Promise<{
|
||||
canceled: boolean;
|
||||
filePath?: string;
|
||||
}> => {
|
||||
return ipcRenderer.invoke('show-save-dialog', { defaultPath });
|
||||
};
|
||||
|
||||
async function writeWithAttributes(
|
||||
target: string,
|
||||
data: Uint8Array
|
||||
): Promise<void> {
|
||||
await fse.writeFile(target, 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(target, 'com.apple.quarantine', attrValue);
|
||||
} else if (isWindows()) {
|
||||
// This operation may fail (see the function's comments), which is not a show-stopper.
|
||||
try {
|
||||
await writeWindowsZoneIdentifier(target);
|
||||
} catch (err) {
|
||||
window.SignalContext.log?.warn(
|
||||
'Failed to write Windows Zone.Identifier file; continuing'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const saveAttachmentToDisk = async ({
|
||||
data,
|
||||
name,
|
||||
}: {
|
||||
data: Uint8Array;
|
||||
name: string;
|
||||
}): Promise<null | { fullPath: string; name: string }> => {
|
||||
const { canceled, filePath } = await showSaveDialog(name);
|
||||
|
||||
if (canceled || !filePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await writeWithAttributes(filePath, data);
|
||||
|
||||
const fileBasename = basename(filePath);
|
||||
|
||||
return {
|
||||
fullPath: filePath,
|
||||
name: fileBasename,
|
||||
};
|
||||
};
|
|
@ -53,6 +53,7 @@ export type SignalContextType = {
|
|||
getEnvironment: () => string;
|
||||
getNodeVersion: () => string;
|
||||
getVersion: () => string;
|
||||
getPath: (name: 'userData' | 'home' | 'downloads') => string;
|
||||
i18n: LocalizerType;
|
||||
log: LoggerType;
|
||||
renderWindow?: () => void;
|
||||
|
@ -71,6 +72,9 @@ export const SignalContext: SignalContextType = {
|
|||
getEnvironment,
|
||||
getNodeVersion: (): string => String(config.node_version),
|
||||
getVersion: (): string => String(config.version),
|
||||
getPath: (name: 'userData' | 'home' | 'downloads'): string => {
|
||||
return String(config[`${name}Path`]);
|
||||
},
|
||||
i18n: setupI18n(locale, localeMessages),
|
||||
log: window.SignalContext.log,
|
||||
nativeThemeListener: createNativeThemeListener(ipcRenderer, window),
|
||||
|
|
Loading…
Reference in a new issue