Get rid of electron.remote
This commit is contained in:
parent
246583d274
commit
76d8b5e375
16 changed files with 563 additions and 591 deletions
293
ts/test-electron/windows/attachments_test.ts
Normal file
293
ts/test-electron/windows/attachments_test.ts
Normal file
|
@ -0,0 +1,293 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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;
|
||||
const NAME_LENGTH = 64;
|
||||
const PATH_LENGTH = PREFIX_LENGTH + NUM_SEPARATORS + NAME_LENGTH;
|
||||
|
||||
describe('Attachments', () => {
|
||||
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(() => {
|
||||
filesToRemove = [];
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await Promise.all(filesToRemove.map(toRemove => fse.remove(toRemove)));
|
||||
filesToRemove = [];
|
||||
});
|
||||
|
||||
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'
|
||||
);
|
||||
|
||||
assert.strictEqual(size, 'hello world'.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createWriterForExisting', () => {
|
||||
it('should write file to disk on given path and return path', async () => {
|
||||
const input = Bytes.fromString('test string');
|
||||
const tempDirectory = path.join(
|
||||
tempRootDirectory,
|
||||
'Attachments_createWriterForExisting'
|
||||
);
|
||||
|
||||
const relativePath = Attachments.getRelativePath(
|
||||
Attachments.createName()
|
||||
);
|
||||
const attachment = {
|
||||
path: relativePath,
|
||||
data: input,
|
||||
};
|
||||
const outputPath = await Attachments.createWriterForExisting(
|
||||
tempDirectory
|
||||
)(attachment);
|
||||
const output = await fse.readFile(path.join(tempDirectory, outputPath));
|
||||
|
||||
assert.equal(outputPath, relativePath);
|
||||
|
||||
const inputBuffer = Buffer.from(input);
|
||||
assert.deepEqual(inputBuffer, output);
|
||||
});
|
||||
|
||||
it('throws if relative path goes higher than root', async () => {
|
||||
const input = Bytes.fromString('test string');
|
||||
const tempDirectory = path.join(
|
||||
tempRootDirectory,
|
||||
'Attachments_createWriterForExisting'
|
||||
);
|
||||
|
||||
const relativePath = '../../parent';
|
||||
const attachment = {
|
||||
path: relativePath,
|
||||
data: input,
|
||||
};
|
||||
try {
|
||||
await Attachments.createWriterForExisting(tempDirectory)(attachment);
|
||||
} catch (error) {
|
||||
assert.strictEqual(error.message, 'Invalid relative path');
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Expected an error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createWriterForNew', () => {
|
||||
it('should write file to disk and return path', async () => {
|
||||
const input = Bytes.fromString('test string');
|
||||
const tempDirectory = path.join(
|
||||
tempRootDirectory,
|
||||
'Attachments_createWriterForNew'
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAbsolutePathGetter', () => {
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
it('combines root and relative path', () => {
|
||||
const root = isWindows ? 'C:\\temp' : '/tmp';
|
||||
const relative = 'ab/abcdef';
|
||||
const pathGetter = Attachments.createAbsolutePathGetter(root);
|
||||
const absolutePath = pathGetter(relative);
|
||||
|
||||
assert.strictEqual(
|
||||
absolutePath,
|
||||
isWindows ? 'C:\\temp\\ab\\abcdef' : '/tmp/ab/abcdef'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if relative path goes higher than root', () => {
|
||||
const root = isWindows ? 'C:\\temp' : 'tmp';
|
||||
const relative = '../../ab/abcdef';
|
||||
const pathGetter = Attachments.createAbsolutePathGetter(root);
|
||||
|
||||
try {
|
||||
pathGetter(relative);
|
||||
} 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);
|
||||
});
|
||||
});
|
||||
|
||||
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…
Add table
Add a link
Reference in a new issue