mirror of
https://github.com/standardnotes/app
synced 2026-09-20 21:13:51 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4b858f2e8 | ||
|
|
325737bfbd | ||
|
|
d057cdff84 | ||
|
|
0528c06ce8 | ||
|
|
bd906f2a12 | ||
|
|
669b395709 | ||
|
|
da4967434a | ||
|
|
96897163fb | ||
|
|
3dc179c7b0 | ||
|
|
1f5354c816 | ||
|
|
f097f6e354 | ||
|
|
97456f7c0b | ||
|
|
937f9d98d7 | ||
|
|
07410cefb7 | ||
|
|
de6994f743 | ||
|
|
5c87f3a3b4 | ||
|
|
b257af17aa | ||
|
|
3a75afc817 | ||
|
|
e4488dcb8c | ||
|
|
bb997312e8 | ||
|
|
b229a96e58 | ||
|
|
7f0bb4e127 | ||
|
|
bf9449b67d | ||
|
|
67359801bb | ||
|
|
53e978c30e | ||
|
|
ebc9fcb995 | ||
|
|
7923e9f198 | ||
|
|
9081607dc7 | ||
|
|
c8e52b667c | ||
|
|
5c6ccaf4e1 | ||
|
|
0ab6b5c6fe | ||
|
|
2a050fd966 | ||
|
|
40f0d3d5bf | ||
|
|
c9c93d143a | ||
|
|
a52828cd45 | ||
|
|
2611c88cee | ||
|
|
d4db3e055c | ||
|
|
05528782a4 | ||
|
|
9664d31c4e | ||
|
|
b06999d25b | ||
|
|
b4a90025c4 | ||
|
|
31b42553be | ||
|
|
41311801c0 | ||
|
|
ccc9d73b3b | ||
|
|
1f144e062e | ||
|
|
40f8985ede | ||
|
|
27fc4be886 | ||
|
|
96f42643a9 | ||
|
|
d79e7b14b1 | ||
|
|
6d5cbcb396 | ||
|
|
20bbd70595 | ||
|
|
5839f7d075 | ||
|
|
6a6cbe57a1 | ||
|
|
a8bc20e99e | ||
|
|
d42acab032 | ||
|
|
efa0669dbc | ||
|
|
a6d7844ad4 | ||
|
|
929642195d | ||
|
|
f354a50c26 | ||
|
|
ab1937226f | ||
|
|
64729a816c | ||
|
|
c9a95c818b |
@@ -17,6 +17,7 @@ on:
|
||||
- 'packages/sncrypto-web/**'
|
||||
- 'packages/snjs/**'
|
||||
- 'packages/utils/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,6 +3,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.26.26](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.26.25](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.26.24](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.26.23](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.26.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.26.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.26.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.26.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.26.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
@@ -6,4 +6,5 @@ module.exports = {
|
||||
transform: {
|
||||
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }],
|
||||
},
|
||||
};
|
||||
coverageThreshold: {},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api",
|
||||
"version": "1.26.18",
|
||||
"version": "1.26.26",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -36,9 +36,8 @@
|
||||
"typescript": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.48.3",
|
||||
"@standardnotes/domain-core": "^1.12.0",
|
||||
"@standardnotes/encryption": "workspace:*",
|
||||
"@standardnotes/common": "^1.50.0",
|
||||
"@standardnotes/domain-core": "^1.22.0",
|
||||
"@standardnotes/models": "workspace:*",
|
||||
"@standardnotes/responses": "workspace:*",
|
||||
"@standardnotes/security": "^1.7.6",
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { Environment } from '@standardnotes/models'
|
||||
import { HttpVerb } from '@standardnotes/responses'
|
||||
import { FetchRequestHandler } from './FetchRequestHandler'
|
||||
import { HttpErrorResponseBody, HttpRequest } from '@standardnotes/responses'
|
||||
|
||||
import { ErrorMessage } from '../Error'
|
||||
|
||||
describe('FetchRequestHandler', () => {
|
||||
const snjsVersion = 'snjsVersion'
|
||||
const appVersion = 'appVersion'
|
||||
const environment = Environment.Web
|
||||
const requestHandler = new FetchRequestHandler(snjsVersion, appVersion, environment)
|
||||
|
||||
it('should create a request', () => {
|
||||
const httpRequest: HttpRequest = {
|
||||
url: 'http://localhost:3000/test',
|
||||
verb: HttpVerb.Get,
|
||||
external: false,
|
||||
authentication: 'authentication',
|
||||
customHeaders: [],
|
||||
params: {
|
||||
key: 'value',
|
||||
},
|
||||
}
|
||||
|
||||
const request = requestHandler['createRequest'](httpRequest)
|
||||
|
||||
expect(request).toBeInstanceOf(Request)
|
||||
expect(request.url).toBe(httpRequest.url)
|
||||
expect(request.method).toBe(httpRequest.verb)
|
||||
expect(request.headers.get('X-SNJS-Version')).toBe(snjsVersion)
|
||||
expect(request.headers.get('X-Application-Version')).toBe(`${Environment[environment]}-${appVersion}`)
|
||||
expect(request.headers.get('Content-Type')).toBe('application/json')
|
||||
})
|
||||
|
||||
it('should get url for url and params', () => {
|
||||
const urlWithoutExistingParams = requestHandler['urlForUrlAndParams']('url', { key: 'value' })
|
||||
expect(urlWithoutExistingParams).toBe('url?key=value')
|
||||
|
||||
const urlWithExistingParams = requestHandler['urlForUrlAndParams']('url?key=value', { key2: 'value2' })
|
||||
expect(urlWithExistingParams).toBe('url?key=value&key2=value2')
|
||||
})
|
||||
|
||||
it('should create request body if not GET', () => {
|
||||
const body = requestHandler['createRequestBody']({
|
||||
url: 'url',
|
||||
verb: HttpVerb.Post,
|
||||
external: false,
|
||||
authentication: 'authentication',
|
||||
customHeaders: [],
|
||||
params: {
|
||||
key: 'value',
|
||||
},
|
||||
})
|
||||
|
||||
expect(body).toBe('{"key":"value"}')
|
||||
})
|
||||
|
||||
it('should not create request body if GET', () => {
|
||||
const body = requestHandler['createRequestBody']({
|
||||
url: 'url',
|
||||
verb: HttpVerb.Get,
|
||||
external: false,
|
||||
authentication: 'authentication',
|
||||
customHeaders: [],
|
||||
params: {
|
||||
key: 'value',
|
||||
},
|
||||
})
|
||||
|
||||
expect(body).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle json response', async () => {
|
||||
const fetchResponse = new Response('{"key":"value"}', {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
const response = await requestHandler['handleFetchResponse'](fetchResponse)
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
headers: new Map<string, string | null>([['content-type', 'application/json']]),
|
||||
data: {
|
||||
key: 'value',
|
||||
},
|
||||
key: 'value',
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle non-json response', async () => {
|
||||
const fetchResponse = new Response('body', {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
})
|
||||
|
||||
const response = await requestHandler['handleFetchResponse'](fetchResponse)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers).toEqual(new Map<string, string | null>([['content-type', 'text/plain']]))
|
||||
expect(response.data).toBeInstanceOf(ArrayBuffer)
|
||||
})
|
||||
|
||||
it('should have ratelimit error when forbidden', async () => {
|
||||
const fetchResponse = new Response('body', {
|
||||
status: 403,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
})
|
||||
|
||||
const response = await requestHandler['handleFetchResponse'](fetchResponse)
|
||||
|
||||
expect(response.status).toBe(403)
|
||||
expect(response.headers).toEqual(new Map<string, string | null>([['content-type', 'text/plain']]))
|
||||
expect((response.data as HttpErrorResponseBody).error).toEqual({
|
||||
message: ErrorMessage.RateLimited,
|
||||
})
|
||||
})
|
||||
|
||||
describe('should return ErrorResponse when status is not >=200 and <500', () => {
|
||||
it('should add unknown error message when response has no data', async () => {
|
||||
const fetchResponse = new Response('', {
|
||||
status: 599,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
})
|
||||
|
||||
const response = await requestHandler['handleFetchResponse'](fetchResponse)
|
||||
|
||||
expect(response.status).toBe(599)
|
||||
expect(response.headers).toEqual(new Map<string, string | null>([['content-type', 'text/plain']]))
|
||||
expect((response.data as HttpErrorResponseBody).error).toEqual({
|
||||
message: 'Unknown error',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,178 @@
|
||||
import {
|
||||
HttpErrorResponse,
|
||||
HttpRequest,
|
||||
HttpRequestParams,
|
||||
HttpResponse,
|
||||
HttpStatusCode,
|
||||
HttpVerb,
|
||||
isErrorResponse,
|
||||
} from '@standardnotes/responses'
|
||||
import { RequestHandlerInterface } from './RequestHandlerInterface'
|
||||
import { Environment } from '@standardnotes/models'
|
||||
import { isString } from 'lodash'
|
||||
import { ErrorMessage } from '../Error'
|
||||
|
||||
export class FetchRequestHandler implements RequestHandlerInterface {
|
||||
constructor(
|
||||
protected readonly snjsVersion: string,
|
||||
protected readonly appVersion: string,
|
||||
protected readonly environment: Environment,
|
||||
) {}
|
||||
|
||||
async handleRequest<T>(httpRequest: HttpRequest): Promise<HttpResponse<T>> {
|
||||
const request = this.createRequest(httpRequest)
|
||||
|
||||
const response = await this.runRequest<T>(request, this.createRequestBody(httpRequest))
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
private createRequest(httpRequest: HttpRequest): Request {
|
||||
if (httpRequest.params && httpRequest.verb === HttpVerb.Get && Object.keys(httpRequest.params).length > 0) {
|
||||
httpRequest.url = this.urlForUrlAndParams(httpRequest.url, httpRequest.params)
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {}
|
||||
|
||||
if (!httpRequest.external) {
|
||||
headers['X-SNJS-Version'] = this.snjsVersion
|
||||
|
||||
const appVersionHeaderValue = `${Environment[this.environment]}-${this.appVersion}`
|
||||
headers['X-Application-Version'] = appVersionHeaderValue
|
||||
|
||||
if (httpRequest.authentication) {
|
||||
headers['Authorization'] = 'Bearer ' + httpRequest.authentication
|
||||
}
|
||||
}
|
||||
|
||||
let contentTypeIsSet = false
|
||||
if (httpRequest.customHeaders && httpRequest.customHeaders.length > 0) {
|
||||
httpRequest.customHeaders.forEach(({ key, value }) => {
|
||||
headers[key] = value
|
||||
if (key === 'Content-Type') {
|
||||
contentTypeIsSet = true
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!contentTypeIsSet && !httpRequest.external) {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
return new Request(httpRequest.url, {
|
||||
method: httpRequest.verb,
|
||||
headers,
|
||||
})
|
||||
}
|
||||
|
||||
private async runRequest<T>(request: Request, body?: string | Uint8Array | undefined): Promise<HttpResponse<T>> {
|
||||
const fetchResponse = await fetch(request, {
|
||||
body,
|
||||
})
|
||||
|
||||
const response = await this.handleFetchResponse<T>(fetchResponse)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
private async handleFetchResponse<T>(fetchResponse: Response): Promise<HttpResponse<T>> {
|
||||
const httpStatus = fetchResponse.status
|
||||
const response: HttpResponse<T> = {
|
||||
status: httpStatus,
|
||||
headers: new Map<string, string | null>(),
|
||||
data: {} as T,
|
||||
}
|
||||
fetchResponse.headers.forEach((value, key) => {
|
||||
;(<Map<string, string | null>>response.headers).set(key, value)
|
||||
})
|
||||
|
||||
try {
|
||||
if (httpStatus !== HttpStatusCode.NoContent) {
|
||||
let body
|
||||
|
||||
const contentTypeHeader = response.headers?.get('content-type') || response.headers?.get('Content-Type')
|
||||
|
||||
if (contentTypeHeader?.includes('application/json')) {
|
||||
body = JSON.parse(await fetchResponse.text())
|
||||
} else {
|
||||
body = await fetchResponse.arrayBuffer()
|
||||
}
|
||||
/**
|
||||
* v0 APIs do not have a `data` top-level object. In such cases, mimic
|
||||
* the newer response body style by putting all the top-level
|
||||
* properties inside a `data` object.
|
||||
*/
|
||||
if (!body.data) {
|
||||
response.data = body
|
||||
}
|
||||
if (!isString(body)) {
|
||||
Object.assign(response, body)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
if (httpStatus >= HttpStatusCode.Success && httpStatus < HttpStatusCode.InternalServerError) {
|
||||
if (httpStatus === HttpStatusCode.Forbidden && isErrorResponse(response)) {
|
||||
if (!response.data.error) {
|
||||
response.data.error = {
|
||||
message: ErrorMessage.RateLimited,
|
||||
}
|
||||
} else {
|
||||
response.data.error.message = ErrorMessage.RateLimited
|
||||
}
|
||||
}
|
||||
return response
|
||||
} else {
|
||||
const errorResponse = response as HttpErrorResponse
|
||||
if (!errorResponse.data) {
|
||||
errorResponse.data = {
|
||||
error: {
|
||||
message: 'Unknown error',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (isString(errorResponse.data)) {
|
||||
errorResponse.data = {
|
||||
error: {
|
||||
message: errorResponse.data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (!errorResponse.data.error) {
|
||||
errorResponse.data.error = {
|
||||
message: 'Unknown error',
|
||||
}
|
||||
}
|
||||
|
||||
return errorResponse
|
||||
}
|
||||
}
|
||||
|
||||
private urlForUrlAndParams(url: string, params: HttpRequestParams) {
|
||||
const keyValueString = Object.keys(params as Record<string, unknown>)
|
||||
.map((key) => {
|
||||
return key + '=' + encodeURIComponent((params as Record<string, unknown>)[key] as string)
|
||||
})
|
||||
.join('&')
|
||||
|
||||
if (url.includes('?')) {
|
||||
return url + '&' + keyValueString
|
||||
} else {
|
||||
return url + '?' + keyValueString
|
||||
}
|
||||
}
|
||||
|
||||
private createRequestBody(httpRequest: HttpRequest): string | Uint8Array | undefined {
|
||||
if (
|
||||
httpRequest.params !== undefined &&
|
||||
[HttpVerb.Post, HttpVerb.Put, HttpVerb.Patch, HttpVerb.Delete].includes(httpRequest.verb)
|
||||
) {
|
||||
return JSON.stringify(httpRequest.params)
|
||||
}
|
||||
|
||||
return httpRequest.rawBytes
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isString, joinPaths, sleep } from '@standardnotes/utils'
|
||||
import { joinPaths, sleep } from '@standardnotes/utils'
|
||||
import { Environment } from '@standardnotes/models'
|
||||
import { Session, SessionToken } from '@standardnotes/domain-core'
|
||||
import {
|
||||
@@ -8,15 +8,14 @@ import {
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
HttpResponseMeta,
|
||||
HttpErrorResponse,
|
||||
isErrorResponse,
|
||||
} from '@standardnotes/responses'
|
||||
import { HttpServiceInterface } from './HttpServiceInterface'
|
||||
import { XMLHttpRequestState } from './XMLHttpRequestState'
|
||||
import { ErrorMessage } from '../Error/ErrorMessage'
|
||||
|
||||
import { Paths } from '../Server/Auth/Paths'
|
||||
import { SessionRefreshResponseBody } from '../Response/Auth/SessionRefreshResponseBody'
|
||||
import { FetchRequestHandler } from './FetchRequestHandler'
|
||||
import { RequestHandlerInterface } from './RequestHandlerInterface'
|
||||
|
||||
export class HttpService implements HttpServiceInterface {
|
||||
private session: Session | null
|
||||
@@ -27,8 +26,11 @@ export class HttpService implements HttpServiceInterface {
|
||||
private updateMetaCallback!: (meta: HttpResponseMeta) => void
|
||||
private refreshSessionCallback!: (session: Session) => void
|
||||
|
||||
private requestHandler: RequestHandlerInterface
|
||||
|
||||
constructor(private environment: Environment, private appVersion: string, private snjsVersion: string) {
|
||||
this.session = null
|
||||
this.requestHandler = new FetchRequestHandler(this.snjsVersion, this.appVersion, this.environment)
|
||||
}
|
||||
|
||||
setCallbacks(
|
||||
@@ -131,9 +133,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
httpRequest.authentication = this.session?.accessToken.value
|
||||
}
|
||||
|
||||
const request = this.createXmlRequest(httpRequest)
|
||||
|
||||
const response = await this.runRequest<T>(request, this.createRequestBody(httpRequest))
|
||||
const response = await this.requestHandler.handleRequest<T>(httpRequest)
|
||||
|
||||
if (response.meta && !httpRequest.external) {
|
||||
this.updateMetaCallback?.(response.meta)
|
||||
@@ -209,159 +209,4 @@ export class HttpService implements HttpServiceInterface {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private createRequestBody(httpRequest: HttpRequest): string | Uint8Array | undefined {
|
||||
if (
|
||||
httpRequest.params !== undefined &&
|
||||
[HttpVerb.Post, HttpVerb.Put, HttpVerb.Patch, HttpVerb.Delete].includes(httpRequest.verb)
|
||||
) {
|
||||
return JSON.stringify(httpRequest.params)
|
||||
}
|
||||
|
||||
return httpRequest.rawBytes
|
||||
}
|
||||
|
||||
private createXmlRequest(httpRequest: HttpRequest) {
|
||||
const request = new XMLHttpRequest()
|
||||
if (httpRequest.params && httpRequest.verb === HttpVerb.Get && Object.keys(httpRequest.params).length > 0) {
|
||||
httpRequest.url = this.urlForUrlAndParams(httpRequest.url, httpRequest.params)
|
||||
}
|
||||
request.open(httpRequest.verb, httpRequest.url, true)
|
||||
request.responseType = httpRequest.responseType ?? ''
|
||||
|
||||
if (!httpRequest.external) {
|
||||
request.setRequestHeader('X-SNJS-Version', this.snjsVersion)
|
||||
|
||||
const appVersionHeaderValue = `${Environment[this.environment]}-${this.appVersion}`
|
||||
request.setRequestHeader('X-Application-Version', appVersionHeaderValue)
|
||||
|
||||
if (httpRequest.authentication) {
|
||||
request.setRequestHeader('Authorization', 'Bearer ' + httpRequest.authentication)
|
||||
}
|
||||
}
|
||||
|
||||
let contenTypeIsSet = false
|
||||
if (httpRequest.customHeaders && httpRequest.customHeaders.length > 0) {
|
||||
httpRequest.customHeaders.forEach(({ key, value }) => {
|
||||
request.setRequestHeader(key, value)
|
||||
if (key === 'Content-Type') {
|
||||
contenTypeIsSet = true
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!contenTypeIsSet && !httpRequest.external) {
|
||||
request.setRequestHeader('Content-Type', 'application/json')
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
private async runRequest<T>(request: XMLHttpRequest, body?: string | Uint8Array): Promise<HttpResponse<T>> {
|
||||
return new Promise((resolve) => {
|
||||
request.onreadystatechange = () => {
|
||||
this.stateChangeHandlerForRequest(request, resolve)
|
||||
}
|
||||
request.send(body)
|
||||
})
|
||||
}
|
||||
|
||||
private stateChangeHandlerForRequest<T>(request: XMLHttpRequest, resolve: (response: HttpResponse<T>) => void) {
|
||||
if (request.readyState !== XMLHttpRequestState.Completed) {
|
||||
return
|
||||
}
|
||||
const httpStatus = request.status
|
||||
const response: HttpResponse<T> = {
|
||||
status: httpStatus,
|
||||
headers: new Map<string, string | null>(),
|
||||
data: {} as T,
|
||||
}
|
||||
|
||||
const responseHeaderLines = request
|
||||
.getAllResponseHeaders()
|
||||
?.trim()
|
||||
.split(/[\r\n]+/)
|
||||
responseHeaderLines?.forEach((responseHeaderLine) => {
|
||||
const parts = responseHeaderLine.split(': ')
|
||||
const name = parts.shift() as string
|
||||
const value = parts.join(': ')
|
||||
|
||||
;(<Map<string, string | null>>response.headers).set(name, value)
|
||||
})
|
||||
|
||||
try {
|
||||
if (httpStatus !== HttpStatusCode.NoContent) {
|
||||
let body
|
||||
|
||||
const contentTypeHeader = response.headers?.get('content-type') || response.headers?.get('Content-Type')
|
||||
|
||||
if (contentTypeHeader?.includes('application/json')) {
|
||||
body = JSON.parse(request.responseText)
|
||||
} else {
|
||||
body = request.response
|
||||
}
|
||||
/**
|
||||
* v0 APIs do not have a `data` top-level object. In such cases, mimic
|
||||
* the newer response body style by putting all the top-level
|
||||
* properties inside a `data` object.
|
||||
*/
|
||||
if (!body.data) {
|
||||
response.data = body
|
||||
}
|
||||
if (!isString(body)) {
|
||||
Object.assign(response, body)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
if (httpStatus >= HttpStatusCode.Success && httpStatus < HttpStatusCode.InternalServerError) {
|
||||
if (
|
||||
httpStatus === HttpStatusCode.Forbidden &&
|
||||
response.data &&
|
||||
(response as HttpErrorResponse).data.error !== undefined
|
||||
) {
|
||||
;(response as HttpErrorResponse).data.error.message = ErrorMessage.RateLimited
|
||||
}
|
||||
resolve(response)
|
||||
} else {
|
||||
const errorResponse = response as HttpErrorResponse
|
||||
if (!errorResponse.data) {
|
||||
errorResponse.data = {
|
||||
error: {
|
||||
message: 'Unknown error',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (isString(errorResponse.data)) {
|
||||
errorResponse.data = {
|
||||
error: {
|
||||
message: errorResponse.data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (!errorResponse.data.error) {
|
||||
errorResponse.data.error = {
|
||||
message: 'Unknown error',
|
||||
}
|
||||
}
|
||||
|
||||
resolve(errorResponse)
|
||||
}
|
||||
}
|
||||
|
||||
private urlForUrlAndParams(url: string, params: HttpRequestParams) {
|
||||
const keyValueString = Object.keys(params as Record<string, unknown>)
|
||||
.map((key) => {
|
||||
return key + '=' + encodeURIComponent((params as Record<string, unknown>)[key] as string)
|
||||
})
|
||||
.join('&')
|
||||
|
||||
if (url.includes('?')) {
|
||||
return url + '&' + keyValueString
|
||||
} else {
|
||||
return url + '?' + keyValueString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { HttpRequest, HttpResponse } from '@standardnotes/responses'
|
||||
|
||||
export interface RequestHandlerInterface {
|
||||
handleRequest<T>(httpRequest: HttpRequest): Promise<HttpResponse<T>>
|
||||
}
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
import { ApiEndpointParam } from '@standardnotes/responses'
|
||||
import { ApiVersion } from '../../Api/ApiVersion'
|
||||
|
||||
export type SubscriptionInviteCancelRequestParams = {
|
||||
[ApiEndpointParam.ApiVersion]: ApiVersion.v0
|
||||
[ApiEndpointParam.ApiVersion]: string
|
||||
inviteUuid: string
|
||||
[additionalParam: string]: unknown
|
||||
}
|
||||
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
import { ApiEndpointParam } from '@standardnotes/responses'
|
||||
import { ApiVersion } from '../../Api/ApiVersion'
|
||||
|
||||
export type SubscriptionInviteDeclineRequestParams = {
|
||||
[ApiEndpointParam.ApiVersion]: ApiVersion.v0
|
||||
[ApiEndpointParam.ApiVersion]: string
|
||||
inviteUuid: string
|
||||
[additionalParam: string]: unknown
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ApiEndpointParam } from '@standardnotes/responses'
|
||||
import { ApiVersion } from '../../Api/ApiVersion'
|
||||
|
||||
export type SubscriptionInviteListRequestParams = {
|
||||
[ApiEndpointParam.ApiVersion]: ApiVersion.v0
|
||||
[ApiEndpointParam.ApiVersion]: string
|
||||
[additionalParam: string]: unknown
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ApiEndpointParam } from '@standardnotes/responses'
|
||||
import { ApiVersion } from '../../Api/ApiVersion'
|
||||
|
||||
export type SubscriptionInviteRequestParams = {
|
||||
[ApiEndpointParam.ApiVersion]: ApiVersion.v0
|
||||
[ApiEndpointParam.ApiVersion]: string
|
||||
identifier: string
|
||||
[additionalParam: string]: unknown
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { AnyKeyParamsContent } from '@standardnotes/common'
|
||||
import { ApiEndpointParam } from '@standardnotes/responses'
|
||||
import { ApiVersion } from '../../Api/ApiVersion'
|
||||
|
||||
export type UserRegistrationRequestParams = AnyKeyParamsContent & {
|
||||
[ApiEndpointParam.ApiVersion]: ApiVersion.v0
|
||||
[ApiEndpointParam.ApiVersion]: string
|
||||
[additionalParam: string]: unknown
|
||||
password: string
|
||||
email: string
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ApiEndpointParam } from '@standardnotes/responses'
|
||||
import { ApiVersion } from '../../Api/ApiVersion'
|
||||
|
||||
export type UserUpdateRequestParams = {
|
||||
[ApiEndpointParam.ApiVersion]: ApiVersion.v0
|
||||
[ApiEndpointParam.ApiVersion]: string
|
||||
user_uuid: string
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ export * from './Subscription/SubscriptionInviteCancelRequestParams'
|
||||
export * from './Subscription/SubscriptionInviteDeclineRequestParams'
|
||||
export * from './Subscription/SubscriptionInviteListRequestParams'
|
||||
export * from './Subscription/SubscriptionInviteRequestParams'
|
||||
export * from './User/UserDeletionRequestParams'
|
||||
export * from './User/UserRegistrationRequestParams'
|
||||
export * from './User/UserUpdateRequestParams'
|
||||
export * from './UserRequest/UserRequestRequestParams'
|
||||
export * from './WebSocket/WebSocketConnectionTokenRequestParams'
|
||||
export * from './SharedVault/SharedVaultMoveType'
|
||||
|
||||
@@ -3,6 +3,110 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.1.84](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.83](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.82](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.81](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.80](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.79](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.78](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.77](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.76](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.75](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.74](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.73](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.72](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.71](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.70](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.69](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.68](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.67](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.66](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.65](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.64](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.63](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.62](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.61](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.60](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.59](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
## [1.1.58](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/clipper
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/clipper",
|
||||
"description": "Web clipper browser extension for Standard Notes",
|
||||
"version": "1.1.58",
|
||||
"version": "1.1.84",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build-mv2": "yarn clean && webpack --config ./webpack.config.prod.js",
|
||||
|
||||
@@ -9,5 +9,7 @@ test/data/tmp/
|
||||
.idea
|
||||
.env
|
||||
|
||||
data/*
|
||||
|
||||
codeqldb
|
||||
yarn-error.log
|
||||
yarn-error.log
|
||||
|
||||
@@ -3,6 +3,114 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.108.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.108.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.108.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.108.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.108.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.108.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.108.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.108.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.108.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.108.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.108.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.108.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.108.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.108.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.108.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
# [3.108.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-04)
|
||||
|
||||
### Features
|
||||
|
||||
* New one-click Home Server, now in Labs. Launch your own self-hosted server instance with just 1 click from the Preferences window. ([#2345](https://github.com/standardnotes/app/issues/2345)) ([0552878](https://github.com/standardnotes/app/commit/05528782a4e9684dbd86fd12b77eb86727d6c414))
|
||||
|
||||
## [3.107.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.107.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.107.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
# [3.107.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-03)
|
||||
|
||||
### Features
|
||||
|
||||
* New one-click Home Server, now in Labs. Launch your own self-hosted server instance with just 1 click from the Preferences window. ([#2341](https://github.com/standardnotes/app/issues/2341)) ([96f4264](https://github.com/standardnotes/app/commit/96f42643a98586052bfb5262ca797fd029c1e925))
|
||||
|
||||
## [3.106.73](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.106.72](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.106.71](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.106.70](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.106.69](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.106.68](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.106.67](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
@@ -28,6 +28,7 @@ export function initializeApplication(args: { app: Electron.App; ipcMain: Electr
|
||||
|
||||
if (isDev()) {
|
||||
/** Expose the app's state as a global variable. Useful for debugging */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(global as any).appState = state
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -124,7 +125,6 @@ async function finishApplicationInitialization({ app, shell, state }: { app: App
|
||||
})
|
||||
|
||||
if (state.isRunningVersionForFirstTime()) {
|
||||
console.log('Clearing window cache')
|
||||
await windowState.window.webContents.session.clearCache()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import path from 'path'
|
||||
import { shell } from 'electron'
|
||||
import { DirectoryManagerInterface } from '@standardnotes/snjs'
|
||||
|
||||
import { FilesManagerInterface } from '../File/FilesManagerInterface'
|
||||
|
||||
export class DirectoryManager implements DirectoryManagerInterface {
|
||||
private lastErrorMessage: string | undefined
|
||||
|
||||
constructor(private filesManager: FilesManagerInterface) {}
|
||||
|
||||
async presentDirectoryPickerForLocationChangeAndTransferOld(
|
||||
appendPath: string,
|
||||
oldLocation?: string,
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
this.lastErrorMessage = undefined
|
||||
|
||||
const selectedDirectory = await this.filesManager.openDirectoryPicker('Select')
|
||||
|
||||
if (!selectedDirectory) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const newPath = path.join(selectedDirectory, path.normalize(appendPath))
|
||||
|
||||
await this.filesManager.ensureDirectoryExists(newPath)
|
||||
|
||||
if (oldLocation) {
|
||||
const result = await this.filesManager.moveDirectory(path.normalize(oldLocation), newPath)
|
||||
if (result.isFailed()) {
|
||||
this.lastErrorMessage = result.getError()
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const deletingDirectoryResult = await this.filesManager.deleteDir(path.normalize(oldLocation))
|
||||
if (deletingDirectoryResult.isFailed()) {
|
||||
this.lastErrorMessage = deletingDirectoryResult.getError()
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
return newPath
|
||||
} catch (error) {
|
||||
this.lastErrorMessage = (error as Error).message
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
async openLocation(location: string): Promise<void> {
|
||||
void shell.openPath(location)
|
||||
}
|
||||
|
||||
async getDirectoryManagerLastErrorMessage(): Promise<string | undefined> {
|
||||
return this.lastErrorMessage
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import path from 'path'
|
||||
import { URL } from 'url'
|
||||
import { extensions as str } from './Strings'
|
||||
import { Paths } from './Types/Paths'
|
||||
import { FileDoesNotExist } from './Utils/FileUtils'
|
||||
import { app } from 'electron'
|
||||
import { FileErrorCodes } from './File/FileErrorCodes'
|
||||
|
||||
const Protocol = 'http'
|
||||
|
||||
@@ -80,7 +80,7 @@ function onRequestError(error: Error | { code: string }, response: ServerRespons
|
||||
let responseCode: number
|
||||
let message: string
|
||||
|
||||
if ('code' in error && error.code === FileDoesNotExist) {
|
||||
if ('code' in error && error.code === FileErrorCodes.FileDoesNotExist) {
|
||||
responseCode = 404
|
||||
message = str().missingExtension
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum FileErrorCodes {
|
||||
FileDoesNotExist = 'ENOENT',
|
||||
FileAlreadyExists = 'EEXIST',
|
||||
OperationNotPermitted = 'EPERM',
|
||||
DeviceIsBusy = 'EBUSY',
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
import { dialog } from 'electron'
|
||||
import fs, { PathLike } from 'fs'
|
||||
import fse from 'fs-extra'
|
||||
import { debounce } from 'lodash'
|
||||
import path from 'path'
|
||||
import yauzl from 'yauzl'
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
import { removeFromArray } from '../Utils/Utils'
|
||||
|
||||
import { FileErrorCodes } from './FileErrorCodes'
|
||||
import { FilesManagerInterface } from './FilesManagerInterface'
|
||||
|
||||
export class FilesManager implements FilesManagerInterface {
|
||||
debouncedJSONDiskWriter(durationMs: number, location: string, data: () => unknown): () => void {
|
||||
let writingToDisk = false
|
||||
return debounce(async () => {
|
||||
if (writingToDisk) {
|
||||
return
|
||||
}
|
||||
writingToDisk = true
|
||||
try {
|
||||
await this.writeJSONFile(location, data())
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
writingToDisk = false
|
||||
}
|
||||
}, durationMs)
|
||||
}
|
||||
|
||||
async openDirectoryPicker(buttonLabel?: string): Promise<string | undefined> {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openDirectory', 'showHiddenFiles', 'createDirectory'],
|
||||
buttonLabel: buttonLabel,
|
||||
})
|
||||
|
||||
return result.filePaths[0]
|
||||
}
|
||||
|
||||
async readJSONFile<T>(filepath: string): Promise<T | undefined> {
|
||||
try {
|
||||
const data = await fs.promises.readFile(filepath, 'utf8')
|
||||
return JSON.parse(data)
|
||||
} catch (error) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
readJSONFileSync<T>(filepath: string): T {
|
||||
const data = fs.readFileSync(filepath, 'utf8')
|
||||
return JSON.parse(data)
|
||||
}
|
||||
|
||||
async writeJSONFile(filepath: string, data: unknown): Promise<void> {
|
||||
await this.ensureDirectoryExists(path.dirname(filepath))
|
||||
await fs.promises.writeFile(filepath, JSON.stringify(data, null, 2), 'utf8')
|
||||
}
|
||||
|
||||
async writeFile(filepath: string, data: string): Promise<void> {
|
||||
await this.ensureDirectoryExists(path.dirname(filepath))
|
||||
await fs.promises.writeFile(filepath, data, 'utf8')
|
||||
}
|
||||
|
||||
writeJSONFileSync(filepath: string, data: unknown): void {
|
||||
fs.writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf8')
|
||||
}
|
||||
|
||||
async ensureDirectoryExists(dirPath: string): Promise<void> {
|
||||
try {
|
||||
const stat = await fs.promises.lstat(dirPath)
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error('Tried to create a directory where a file of the same ' + `name already exists: ${dirPath}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code === FileErrorCodes.FileDoesNotExist) {
|
||||
/**
|
||||
* No directory here. Make sure there is a *parent* directory, and then
|
||||
* create it.
|
||||
*/
|
||||
await this.ensureDirectoryExists(path.dirname(dirPath))
|
||||
|
||||
/** Now that its parent(s) exist, create the directory */
|
||||
try {
|
||||
await fs.promises.mkdir(dirPath)
|
||||
} catch (error: any) {
|
||||
if (error.code === FileErrorCodes.FileAlreadyExists) {
|
||||
/**
|
||||
* A concurrent process must have created the directory already.
|
||||
* Make sure it *is* a directory and not something else.
|
||||
*/
|
||||
await this.ensureDirectoryExists(dirPath)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDir(dirPath: string): Promise<Result<string>> {
|
||||
try {
|
||||
fse.removeSync(dirPath)
|
||||
|
||||
return Result.ok('Directory deleted successfully')
|
||||
} catch (error) {
|
||||
return Result.fail((error as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDirContents(dirPath: string): Promise<void> {
|
||||
/**
|
||||
* Scan the directory up to ten times, to handle cases where files are being added while
|
||||
* the directory's contents are being deleted
|
||||
*/
|
||||
for (let i = 1, maxTries = 10; i < maxTries; i++) {
|
||||
const children = await fs.promises.readdir(dirPath, {
|
||||
withFileTypes: true,
|
||||
})
|
||||
|
||||
if (children.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
for (const child of children) {
|
||||
const childPath = path.join(dirPath, child.name)
|
||||
if (child.isDirectory()) {
|
||||
await this.deleteDirContents(childPath)
|
||||
try {
|
||||
await fs.promises.rmdir(childPath)
|
||||
} catch (error) {
|
||||
if (error !== FileErrorCodes.FileDoesNotExist) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await this.deleteFile(childPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isChildOfDir(parent: string, potentialChild: string): boolean {
|
||||
const relative = path.relative(parent, potentialChild)
|
||||
return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative)
|
||||
}
|
||||
|
||||
async moveDirectory(dir: string, destination: string): Promise<Result<string>> {
|
||||
try {
|
||||
await fse.move(dir, destination, { overwrite: true })
|
||||
|
||||
return Result.ok('Directory moved successfully')
|
||||
} catch (error) {
|
||||
return Result.fail((error as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
async moveDirContents(srcDir: string, destDir: string): Promise<Result<string>> {
|
||||
try {
|
||||
let srcDirectoryContents = await fs.promises.readdir(srcDir)
|
||||
|
||||
await this.ensureDirectoryExists(destDir)
|
||||
|
||||
if (this.isChildOfDir(srcDir, destDir)) {
|
||||
srcDirectoryContents = srcDirectoryContents.filter((name) => {
|
||||
return !this.isChildOfDir(destDir, path.join(srcDir, name))
|
||||
})
|
||||
removeFromArray(srcDirectoryContents, path.basename(destDir))
|
||||
}
|
||||
|
||||
const directoryNames = []
|
||||
const fileNames = []
|
||||
for (const contentName of srcDirectoryContents) {
|
||||
const stats = await fs.promises.lstat(path.join(srcDir, contentName))
|
||||
if (stats.isDirectory()) {
|
||||
directoryNames.push(contentName)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
fileNames.push(contentName)
|
||||
}
|
||||
|
||||
for (const directoryName of directoryNames) {
|
||||
const result = await this.moveDirContents(path.join(srcDir, directoryName), path.join(destDir, directoryName))
|
||||
if (result.isFailed()) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
await this.moveFiles(
|
||||
fileNames.map((fileName) => path.join(srcDir, fileName)),
|
||||
destDir,
|
||||
)
|
||||
|
||||
return Result.ok('Directory contents moved successfully')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
return Result.fail(`Could not move directory contentes: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async extractZip(source: string, dest: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
yauzl.open(source, { lazyEntries: true, autoClose: true }, (err, zipFile) => {
|
||||
let cancelled = false
|
||||
|
||||
const tryReject = (err: Error) => {
|
||||
if (!cancelled) {
|
||||
cancelled = true
|
||||
reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return tryReject(err)
|
||||
}
|
||||
|
||||
if (!zipFile) {
|
||||
return tryReject(new Error('zipFile === undefined'))
|
||||
}
|
||||
|
||||
zipFile.readEntry()
|
||||
|
||||
zipFile.on('close', resolve)
|
||||
|
||||
zipFile.on('entry', (entry) => {
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
const isEntryDirectory = entry.fileName.endsWith('/')
|
||||
if (isEntryDirectory) {
|
||||
zipFile.readEntry()
|
||||
return
|
||||
}
|
||||
|
||||
zipFile.openReadStream(entry, async (err, stream) => {
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return tryReject(err)
|
||||
}
|
||||
|
||||
if (!stream) {
|
||||
return tryReject(new Error('stream === undefined'))
|
||||
}
|
||||
|
||||
stream.on('error', tryReject)
|
||||
|
||||
const filepath = path.join(dest, entry.fileName)
|
||||
|
||||
try {
|
||||
await this.ensureDirectoryExists(path.dirname(filepath))
|
||||
} catch (error: any) {
|
||||
return tryReject(error)
|
||||
}
|
||||
const writeStream = fs.createWriteStream(filepath).on('error', tryReject).on('error', tryReject)
|
||||
|
||||
stream.pipe(writeStream).on('close', () => {
|
||||
zipFile.readEntry()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async moveFiles(sources: string[], destDir: string): Promise<void[]> {
|
||||
await this.ensureDirectoryExists(destDir)
|
||||
return Promise.all(sources.map((fileName) => this.moveFile(fileName, path.join(destDir, path.basename(fileName)))))
|
||||
}
|
||||
|
||||
async moveFile(source: PathLike, destination: PathLike): Promise<void> {
|
||||
try {
|
||||
await fs.promises.rename(source, destination)
|
||||
} catch (_error) {
|
||||
/** Fall back to copying and then deleting. */
|
||||
await fs.promises.copyFile(source, destination, fs.constants.COPYFILE_FICLONE_FORCE)
|
||||
await fs.promises.unlink(source)
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFileIfExists(filePath: PathLike): Promise<void> {
|
||||
try {
|
||||
await this.deleteFile(filePath)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFile(filePath: PathLike): Promise<void> {
|
||||
for (let i = 1, maxTries = 10; i < maxTries; i++) {
|
||||
try {
|
||||
await fs.promises.unlink(filePath)
|
||||
break
|
||||
} catch (error: any) {
|
||||
if (error.code === FileErrorCodes.OperationNotPermitted || error.code === FileErrorCodes.DeviceIsBusy) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
continue
|
||||
} else if (error.code === FileErrorCodes.FileDoesNotExist) {
|
||||
/** Already deleted */
|
||||
break
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Result } from '@standardnotes/domain-core'
|
||||
import { PathLike } from 'fs'
|
||||
|
||||
export interface FilesManagerInterface {
|
||||
debouncedJSONDiskWriter(durationMs: number, location: string, data: () => unknown): () => void
|
||||
openDirectoryPicker(buttonLabel?: string): Promise<string | undefined>
|
||||
readJSONFile<T>(filepath: string): Promise<T | undefined>
|
||||
readJSONFileSync<T>(filepath: string): T
|
||||
writeJSONFile(filepath: string, data: unknown): Promise<void>
|
||||
writeFile(filepath: string, data: string): Promise<void>
|
||||
writeJSONFileSync(filepath: string, data: unknown): void
|
||||
ensureDirectoryExists(dirPath: string): Promise<void>
|
||||
deleteDir(dirPath: string): Promise<Result<string>>
|
||||
deleteDirContents(dirPath: string): Promise<void>
|
||||
isChildOfDir(parent: string, potentialChild: string): boolean
|
||||
moveDirectory(dir: string, destination: string): Promise<Result<string>>
|
||||
moveDirContents(srcDir: string, destDir: string): Promise<Result<string>>
|
||||
extractZip(source: string, dest: string): Promise<void>
|
||||
moveFiles(sources: string[], destDir: string): Promise<void[]>
|
||||
moveFile(source: PathLike, destination: PathLike): Promise<void>
|
||||
deleteFileIfExists(filePath: PathLike): Promise<void>
|
||||
deleteFile(filePath: PathLike): Promise<void>
|
||||
}
|
||||
@@ -9,23 +9,14 @@ import {
|
||||
} from '@web/Application/Device/DesktopSnjsExports'
|
||||
import { AppState } from 'app/AppState'
|
||||
import { promises as fs, existsSync } from 'fs'
|
||||
import { WebContents, shell } from 'electron'
|
||||
import { WebContents } from 'electron'
|
||||
import { StoreKeys } from '../Store/StoreKeys'
|
||||
import path from 'path'
|
||||
import {
|
||||
deleteFileIfExists,
|
||||
ensureDirectoryExists,
|
||||
moveDirContents,
|
||||
moveFile,
|
||||
openDirectoryPicker,
|
||||
readJSONFile,
|
||||
writeFile,
|
||||
writeJSONFile,
|
||||
} from '../Utils/FileUtils'
|
||||
import { FileDownloader } from './FileDownloader'
|
||||
import { FileReadOperation } from './FileReadOperation'
|
||||
import { Paths } from '../Types/Paths'
|
||||
import { MessageToWebApp } from '../../Shared/IpcMessages'
|
||||
import { FilesManagerInterface } from '../File/FilesManagerInterface'
|
||||
|
||||
const TextBackupFileExtension = '.txt'
|
||||
|
||||
@@ -39,7 +30,11 @@ export class FilesBackupManager implements FileBackupsDevice {
|
||||
private readOperations: Map<string, FileReadOperation> = new Map()
|
||||
private plaintextMappingCache?: PlaintextBackupsMapping
|
||||
|
||||
constructor(private appState: AppState, private webContents: WebContents) {}
|
||||
constructor(
|
||||
private appState: AppState,
|
||||
private webContents: WebContents,
|
||||
private filesManager: FilesManagerInterface,
|
||||
) {}
|
||||
|
||||
private async findUuidForPlaintextBackupFileName(
|
||||
backupsDirectory: string,
|
||||
@@ -72,16 +67,16 @@ export class FilesBackupManager implements FileBackupsDevice {
|
||||
return
|
||||
}
|
||||
|
||||
await ensureDirectoryExists(newLocation)
|
||||
await this.filesManager.ensureDirectoryExists(newLocation)
|
||||
|
||||
const legacyMappingLocation = path.join(legacyLocation, 'info.json')
|
||||
const newMappingLocation = this.getFileBackupsMappingFilePath(newLocation)
|
||||
await ensureDirectoryExists(path.dirname(newMappingLocation))
|
||||
await this.filesManager.ensureDirectoryExists(path.dirname(newMappingLocation))
|
||||
if (existsSync(legacyMappingLocation)) {
|
||||
await moveFile(legacyMappingLocation, newMappingLocation)
|
||||
await this.filesManager.moveFile(legacyMappingLocation, newMappingLocation)
|
||||
}
|
||||
|
||||
await moveDirContents(legacyLocation, newLocation)
|
||||
await this.filesManager.moveDirContents(legacyLocation, newLocation)
|
||||
}
|
||||
|
||||
public async isLegacyFilesBackupsEnabled(): Promise<boolean> {
|
||||
@@ -111,33 +106,12 @@ export class FilesBackupManager implements FileBackupsDevice {
|
||||
return path.join(Paths.homeDir, LegacyTextBackupsDirectory)
|
||||
}
|
||||
|
||||
public async presentDirectoryPickerForLocationChangeAndTransferOld(
|
||||
appendPath: string,
|
||||
oldLocation?: string,
|
||||
): Promise<string | undefined> {
|
||||
const selectedDirectory = await openDirectoryPicker('Select')
|
||||
|
||||
if (!selectedDirectory) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const newPath = path.join(selectedDirectory, path.normalize(appendPath))
|
||||
|
||||
await ensureDirectoryExists(newPath)
|
||||
|
||||
if (oldLocation) {
|
||||
await moveDirContents(path.normalize(oldLocation), newPath)
|
||||
}
|
||||
|
||||
return newPath
|
||||
}
|
||||
|
||||
private getFileBackupsMappingFilePath(backupsLocation: string): string {
|
||||
return path.join(backupsLocation, '.settings', 'info.json')
|
||||
}
|
||||
|
||||
private async getFileBackupsMappingFileFromDisk(backupsLocation: string): Promise<FileBackupsMapping | undefined> {
|
||||
return readJSONFile<FileBackupsMapping>(this.getFileBackupsMappingFilePath(backupsLocation))
|
||||
return this.filesManager.readJSONFile<FileBackupsMapping>(this.getFileBackupsMappingFilePath(backupsLocation))
|
||||
}
|
||||
|
||||
private defaulFileBackupstMappingFileValue(): FileBackupsMapping {
|
||||
@@ -158,12 +132,8 @@ export class FilesBackupManager implements FileBackupsDevice {
|
||||
return data
|
||||
}
|
||||
|
||||
async openLocation(location: string): Promise<void> {
|
||||
void shell.openPath(location)
|
||||
}
|
||||
|
||||
private async saveFilesBackupsMappingFile(location: string, file: FileBackupsMapping): Promise<'success' | 'failed'> {
|
||||
await writeJSONFile(this.getFileBackupsMappingFilePath(location), file)
|
||||
await this.filesManager.writeJSONFile(this.getFileBackupsMappingFilePath(location), file)
|
||||
|
||||
return 'success'
|
||||
}
|
||||
@@ -182,9 +152,9 @@ export class FilesBackupManager implements FileBackupsDevice {
|
||||
const metaFilePath = path.join(fileDir, FileBackupsConstantsV1.MetadataFileName)
|
||||
const binaryPath = path.join(fileDir, FileBackupsConstantsV1.BinaryFileName)
|
||||
|
||||
await ensureDirectoryExists(fileDir)
|
||||
await this.filesManager.ensureDirectoryExists(fileDir)
|
||||
|
||||
await writeFile(metaFilePath, metaFile)
|
||||
await this.filesManager.writeFile(metaFilePath, metaFile)
|
||||
|
||||
const downloader = new FileDownloader(
|
||||
downloadRequest.chunkSizes,
|
||||
@@ -247,7 +217,7 @@ export class FilesBackupManager implements FileBackupsDevice {
|
||||
let success: boolean
|
||||
|
||||
try {
|
||||
await ensureDirectoryExists(location)
|
||||
await this.filesManager.ensureDirectoryExists(location)
|
||||
const name = `${new Date().toISOString().replace(/:/g, '-')}${TextBackupFileExtension}`
|
||||
const filePath = path.join(location, name)
|
||||
await fs.writeFile(filePath, data)
|
||||
@@ -262,7 +232,7 @@ export class FilesBackupManager implements FileBackupsDevice {
|
||||
|
||||
async copyDecryptScript(location: string) {
|
||||
try {
|
||||
await ensureDirectoryExists(location)
|
||||
await this.filesManager.ensureDirectoryExists(location)
|
||||
await fs.copyFile(Paths.decryptScript, path.join(location, path.basename(Paths.decryptScript)))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -274,14 +244,14 @@ export class FilesBackupManager implements FileBackupsDevice {
|
||||
}
|
||||
|
||||
private async getPlaintextMappingFileFromDisk(location: string): Promise<PlaintextBackupsMapping | undefined> {
|
||||
return readJSONFile<PlaintextBackupsMapping>(this.getPlaintextMappingFilePath(location))
|
||||
return this.filesManager.readJSONFile<PlaintextBackupsMapping>(this.getPlaintextMappingFilePath(location))
|
||||
}
|
||||
|
||||
private async savePlaintextBackupsMappingFile(
|
||||
location: string,
|
||||
file: PlaintextBackupsMapping,
|
||||
): Promise<'success' | 'failed'> {
|
||||
await writeJSONFile(this.getPlaintextMappingFilePath(location), file)
|
||||
await this.filesManager.writeJSONFile(this.getPlaintextMappingFilePath(location), file)
|
||||
|
||||
return 'success'
|
||||
}
|
||||
@@ -324,7 +294,7 @@ export class FilesBackupManager implements FileBackupsDevice {
|
||||
const records = mapping.files[uuid]
|
||||
for (const record of records) {
|
||||
const filePath = path.join(location, record.path)
|
||||
await deleteFileIfExists(filePath)
|
||||
await this.filesManager.deleteFileIfExists(filePath)
|
||||
}
|
||||
mapping.files[uuid] = []
|
||||
}
|
||||
@@ -337,12 +307,12 @@ export class FilesBackupManager implements FileBackupsDevice {
|
||||
return records.find((record) => record.tag === tag)
|
||||
}
|
||||
|
||||
await ensureDirectoryExists(absolutePath)
|
||||
await this.filesManager.ensureDirectoryExists(absolutePath)
|
||||
|
||||
const relativePath = forTag ?? ''
|
||||
const filenameWithSlashesEscaped = filename.replace(/\//g, '\u2215')
|
||||
const fileAbsolutePath = path.join(absolutePath, relativePath, filenameWithSlashesEscaped)
|
||||
await writeFile(fileAbsolutePath, data)
|
||||
await this.filesManager.writeFile(fileAbsolutePath, data)
|
||||
|
||||
const existingRecord = findMappingRecord(forTag)
|
||||
if (!existingRecord) {
|
||||
@@ -385,6 +355,10 @@ export class FilesBackupManager implements FileBackupsDevice {
|
||||
const watcher = fs.watch(backupsDirectory, { recursive: true })
|
||||
for await (const event of watcher) {
|
||||
const { eventType, filename } = event
|
||||
if (!filename) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (eventType !== 'change' && eventType !== 'rename') {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { HomeServerEnvironmentConfiguration } from '@standardnotes/snjs'
|
||||
|
||||
export interface HomeServerConfigurationFile {
|
||||
version: '1.0.0'
|
||||
info: Record<string, string>
|
||||
configuration: HomeServerEnvironmentConfiguration
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import path from 'path'
|
||||
|
||||
import {
|
||||
HomeServerManagerInterface,
|
||||
HomeServerEnvironmentConfiguration,
|
||||
Result,
|
||||
} from '@web/Application/Device/DesktopSnjsExports'
|
||||
|
||||
import { WebContents } from 'electron'
|
||||
import { MessageToWebApp } from '../../Shared/IpcMessages'
|
||||
import { FilesManagerInterface } from '../File/FilesManagerInterface'
|
||||
import { HomeServerConfigurationFile } from './HomeServerConfigurationFile'
|
||||
import { isWindows } from '../Types/Platforms'
|
||||
|
||||
const os = require('os')
|
||||
|
||||
interface TempHomeServerInterface {
|
||||
start(configuration?: unknown): Promise<Result<string>>
|
||||
activatePremiumFeatures(username: string): Promise<Result<string>>
|
||||
stop(): Promise<Result<string>>
|
||||
isRunning(): Promise<boolean>
|
||||
}
|
||||
|
||||
export class HomeServerManager implements HomeServerManagerInterface {
|
||||
private readonly HOME_SERVER_CONFIGURATION_FILE_NAME = 'config.json'
|
||||
|
||||
private homeServerConfiguration: HomeServerEnvironmentConfiguration | undefined
|
||||
private homeServerDataLocation: string | undefined
|
||||
private lastErrorMessage: string | undefined
|
||||
private logs: string[] = []
|
||||
|
||||
private readonly LOGS_BUFFER_SIZE = 1000
|
||||
|
||||
private homeServer?: TempHomeServerInterface
|
||||
|
||||
constructor(private webContents: WebContents, private filesManager: FilesManagerInterface) {}
|
||||
|
||||
async getHomeServerUrl(): Promise<string | undefined> {
|
||||
const homeServerConfiguration = await this.getHomeServerConfigurationObject()
|
||||
if (!homeServerConfiguration) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return `http://${this.getLocalIP()}:${homeServerConfiguration.port}`
|
||||
}
|
||||
|
||||
async isHomeServerRunning(): Promise<boolean> {
|
||||
return this.homeServer?.isRunning() ?? false
|
||||
}
|
||||
|
||||
async getHomeServerLastErrorMessage(): Promise<string | undefined> {
|
||||
return this.lastErrorMessage
|
||||
}
|
||||
|
||||
async activatePremiumFeatures(username: string): Promise<string | undefined> {
|
||||
if (!this.homeServer) {
|
||||
return
|
||||
}
|
||||
|
||||
const result = await this.homeServer.activatePremiumFeatures(username)
|
||||
|
||||
if (result.isFailed()) {
|
||||
return result.getError()
|
||||
}
|
||||
}
|
||||
|
||||
async getHomeServerConfiguration(): Promise<string | undefined> {
|
||||
if (this.homeServerConfiguration) {
|
||||
return JSON.stringify(this.homeServerConfiguration)
|
||||
}
|
||||
|
||||
if (!this.homeServerDataLocation) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const homeServerConfiguration = await this.filesManager.readJSONFile<HomeServerConfigurationFile>(
|
||||
path.join(this.homeServerDataLocation, this.HOME_SERVER_CONFIGURATION_FILE_NAME),
|
||||
)
|
||||
if (!homeServerConfiguration) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return JSON.stringify(homeServerConfiguration.configuration)
|
||||
}
|
||||
|
||||
async setHomeServerConfiguration(configurationJSONString: string): Promise<void> {
|
||||
try {
|
||||
if (!this.homeServerDataLocation) {
|
||||
throw new Error('Home server data location is not set.')
|
||||
}
|
||||
|
||||
const homeServerConfiguration = JSON.parse(configurationJSONString) as HomeServerEnvironmentConfiguration
|
||||
|
||||
await this.filesManager.ensureDirectoryExists(this.homeServerDataLocation)
|
||||
|
||||
const configurationFile: HomeServerConfigurationFile = {
|
||||
version: '1.0.0',
|
||||
info: {
|
||||
warning: 'Do not edit this file.',
|
||||
information:
|
||||
'The values below are encrypted with a key created by the desktop application after installation. The key is stored in your secure device keychain.',
|
||||
instructions:
|
||||
'Put this file inside your home server data location to restore your home server configuration.',
|
||||
},
|
||||
configuration: homeServerConfiguration,
|
||||
}
|
||||
|
||||
await this.filesManager.writeJSONFile(
|
||||
path.join(this.homeServerDataLocation, this.HOME_SERVER_CONFIGURATION_FILE_NAME),
|
||||
configurationFile,
|
||||
)
|
||||
|
||||
this.homeServerConfiguration = homeServerConfiguration
|
||||
} catch (error) {
|
||||
console.error(`Could not save server configuration: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async setHomeServerDataLocation(location: string): Promise<void> {
|
||||
this.homeServerDataLocation = location
|
||||
}
|
||||
|
||||
async stopHomeServer(): Promise<string | undefined> {
|
||||
if (!this.homeServer) {
|
||||
return
|
||||
}
|
||||
|
||||
const result = await this.homeServer.stop()
|
||||
if (result.isFailed()) {
|
||||
return result.getError()
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
async startHomeServer(): Promise<string | undefined> {
|
||||
await this.lazyLoadHomeServerOnApplicablePlatforms()
|
||||
|
||||
if (!this.homeServer) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.lastErrorMessage = undefined
|
||||
this.logs = []
|
||||
|
||||
let homeServerConfiguration = await this.getHomeServerConfigurationObject()
|
||||
if (!homeServerConfiguration) {
|
||||
homeServerConfiguration = this.generateHomeServerConfiguration()
|
||||
}
|
||||
await this.setHomeServerConfiguration(JSON.stringify(homeServerConfiguration))
|
||||
|
||||
if (!this.homeServerDataLocation) {
|
||||
this.lastErrorMessage = 'Home server data location is not set.'
|
||||
|
||||
return this.lastErrorMessage
|
||||
}
|
||||
|
||||
const {
|
||||
jwtSecret,
|
||||
authJwtSecret,
|
||||
encryptionServerKey,
|
||||
pseudoKeyParamsKey,
|
||||
valetTokenSecret,
|
||||
port,
|
||||
logLevel,
|
||||
databaseEngine,
|
||||
mysqlConfiguration,
|
||||
} = homeServerConfiguration as HomeServerEnvironmentConfiguration
|
||||
|
||||
const environment: { [name: string]: string } = {
|
||||
JWT_SECRET: jwtSecret,
|
||||
AUTH_JWT_SECRET: authJwtSecret,
|
||||
ENCRYPTION_SERVER_KEY: encryptionServerKey,
|
||||
PSEUDO_KEY_PARAMS_KEY: pseudoKeyParamsKey,
|
||||
VALET_TOKEN_SECRET: valetTokenSecret,
|
||||
FILES_SERVER_URL: (await this.getHomeServerUrl()) as string,
|
||||
LOG_LEVEL: logLevel ?? 'info',
|
||||
VERSION: 'desktop',
|
||||
PORT: port.toString(),
|
||||
DB_TYPE: databaseEngine,
|
||||
}
|
||||
|
||||
if (mysqlConfiguration !== undefined) {
|
||||
environment.DB_HOST = mysqlConfiguration.host
|
||||
if (mysqlConfiguration.port) {
|
||||
environment.DB_PORT = mysqlConfiguration.port.toString()
|
||||
}
|
||||
environment.DB_USERNAME = mysqlConfiguration.username
|
||||
environment.DB_PASSWORD = mysqlConfiguration.password
|
||||
environment.DB_DATABASE = mysqlConfiguration.database
|
||||
}
|
||||
|
||||
const result = await this.homeServer.start({
|
||||
dataDirectoryPath: this.homeServerDataLocation,
|
||||
environment,
|
||||
logStreamCallback: this.appendLogs.bind(this),
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.lastErrorMessage = result.getError()
|
||||
|
||||
return this.lastErrorMessage
|
||||
}
|
||||
|
||||
this.webContents.send(MessageToWebApp.HomeServerStarted, await this.getHomeServerUrl())
|
||||
} catch (error) {
|
||||
return (error as Error).message
|
||||
}
|
||||
}
|
||||
|
||||
async getHomeServerLogs(): Promise<string[]> {
|
||||
return this.logs
|
||||
}
|
||||
|
||||
private appendLogs(log: Buffer): void {
|
||||
this.logs.push(log.toString())
|
||||
|
||||
if (this.logs.length > this.LOGS_BUFFER_SIZE) {
|
||||
this.logs.shift()
|
||||
}
|
||||
}
|
||||
|
||||
private generateRandomKey(length: number): string {
|
||||
return require('crypto').randomBytes(length).toString('hex')
|
||||
}
|
||||
|
||||
private getLocalIP() {
|
||||
const interfaces = os.networkInterfaces()
|
||||
for (const interfaceName in interfaces) {
|
||||
const addresses = interfaces[interfaceName]
|
||||
for (const address of addresses) {
|
||||
if (address.family === 'IPv4' && !address.internal) {
|
||||
return address.address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getHomeServerConfigurationObject(): Promise<HomeServerEnvironmentConfiguration | undefined> {
|
||||
try {
|
||||
const homeServerConfigurationJSON = await this.getHomeServerConfiguration()
|
||||
if (!homeServerConfigurationJSON) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return JSON.parse(homeServerConfigurationJSON)
|
||||
} catch (error) {
|
||||
console.error(`Could not get home server configuration: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
private generateHomeServerConfiguration(): HomeServerEnvironmentConfiguration {
|
||||
const jwtSecret = this.generateRandomKey(32)
|
||||
const authJwtSecret = this.generateRandomKey(32)
|
||||
const encryptionServerKey = this.generateRandomKey(32)
|
||||
const pseudoKeyParamsKey = this.generateRandomKey(32)
|
||||
const valetTokenSecret = this.generateRandomKey(32)
|
||||
const port = 3127
|
||||
|
||||
const configuration: HomeServerEnvironmentConfiguration = {
|
||||
jwtSecret,
|
||||
authJwtSecret,
|
||||
encryptionServerKey,
|
||||
pseudoKeyParamsKey,
|
||||
valetTokenSecret,
|
||||
port,
|
||||
databaseEngine: 'sqlite',
|
||||
logLevel: 'info',
|
||||
}
|
||||
|
||||
return configuration
|
||||
}
|
||||
|
||||
private async lazyLoadHomeServerOnApplicablePlatforms(): Promise<void> {
|
||||
if (isWindows()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.homeServer) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import path from 'path'
|
||||
import { pipeline as pipelineFn } from 'stream'
|
||||
import { promisify } from 'util'
|
||||
import { MessageType } from '../../../../test/TestIpcMessage'
|
||||
import { ensureDirectoryExists } from '../Utils/FileUtils'
|
||||
import { handleTestMessage } from '../Utils/Testing'
|
||||
import { isTesting } from '../Utils/Utils'
|
||||
import { FilesManager } from '../File/FilesManager'
|
||||
|
||||
const pipeline = promisify(pipelineFn)
|
||||
|
||||
@@ -21,7 +21,7 @@ if (isTesting()) {
|
||||
* not exist)
|
||||
*/
|
||||
export async function downloadFile(url: string, filePath: string): Promise<void> {
|
||||
await ensureDirectoryExists(path.dirname(filePath))
|
||||
await new FilesManager().ensureDirectoryExists(path.dirname(filePath))
|
||||
const response = await get(url)
|
||||
await pipeline(
|
||||
/**
|
||||
|
||||
@@ -5,19 +5,12 @@ import path from 'path'
|
||||
import { MessageToWebApp } from '../../Shared/IpcMessages'
|
||||
import { AppName } from '../Strings'
|
||||
import { Paths } from '../Types/Paths'
|
||||
import {
|
||||
debouncedJSONDiskWriter,
|
||||
deleteDir,
|
||||
deleteDirContents,
|
||||
ensureDirectoryExists,
|
||||
extractZip,
|
||||
FileDoesNotExist,
|
||||
moveDirContents,
|
||||
readJSONFile,
|
||||
} from '../Utils/FileUtils'
|
||||
import { timeout } from '../Utils/Utils'
|
||||
import { downloadFile, getJSON } from './Networking'
|
||||
import { Component, MappingFile, PackageInfo, PackageManagerInterface, SyncTask } from './PackageManagerInterface'
|
||||
import { FilesManagerInterface } from '../File/FilesManagerInterface'
|
||||
import { FilesManager } from '../File/FilesManager'
|
||||
import { FileErrorCodes } from '../File/FileErrorCodes'
|
||||
|
||||
function logMessage(...message: any) {
|
||||
log.info('PackageManager[Info]:', ...message)
|
||||
@@ -33,26 +26,27 @@ function logError(...message: any) {
|
||||
class MappingFileHandler {
|
||||
static async create() {
|
||||
let mapping: MappingFile
|
||||
const filesManager = new FilesManager()
|
||||
|
||||
try {
|
||||
const result = await readJSONFile<MappingFile>(Paths.extensionsMappingJson)
|
||||
const result = await filesManager.readJSONFile<MappingFile>(Paths.extensionsMappingJson)
|
||||
mapping = result || {}
|
||||
} catch (error: any) {
|
||||
/**
|
||||
* Mapping file might be absent (first start, corrupted data)
|
||||
*/
|
||||
if (error.code === FileDoesNotExist) {
|
||||
await ensureDirectoryExists(path.dirname(Paths.extensionsMappingJson))
|
||||
if (error.code === FileErrorCodes.FileDoesNotExist) {
|
||||
await filesManager.ensureDirectoryExists(path.dirname(Paths.extensionsMappingJson))
|
||||
} else {
|
||||
logError(error)
|
||||
}
|
||||
mapping = {}
|
||||
}
|
||||
|
||||
return new MappingFileHandler(mapping)
|
||||
return new MappingFileHandler(mapping, filesManager)
|
||||
}
|
||||
|
||||
constructor(private mapping: MappingFile) {}
|
||||
constructor(private mapping: MappingFile, private filesManager: FilesManagerInterface) {}
|
||||
|
||||
get = (componendId: string) => {
|
||||
return this.mapping[componendId]
|
||||
@@ -63,12 +57,14 @@ class MappingFileHandler {
|
||||
location,
|
||||
version,
|
||||
}
|
||||
this.writeToDisk()
|
||||
|
||||
this.filesManager.debouncedJSONDiskWriter(100, Paths.extensionsMappingJson, () => this.mapping)
|
||||
}
|
||||
|
||||
remove = (componentId: string) => {
|
||||
delete this.mapping[componentId]
|
||||
this.writeToDisk()
|
||||
|
||||
this.filesManager.debouncedJSONDiskWriter(100, Paths.extensionsMappingJson, () => this.mapping)
|
||||
}
|
||||
|
||||
getInstalledVersionForComponent = async (component: Component): Promise<string> => {
|
||||
@@ -83,15 +79,13 @@ class MappingFileHandler {
|
||||
*/
|
||||
const paths = pathsForComponent(component)
|
||||
const packagePath = path.join(paths.absolutePath, 'package.json')
|
||||
const response = await readJSONFile<{ version: string }>(packagePath)
|
||||
const response = await this.filesManager.readJSONFile<{ version: string }>(packagePath)
|
||||
if (!response) {
|
||||
return ''
|
||||
}
|
||||
this.set(component.uuid, paths.relativePath, response.version)
|
||||
return response.version
|
||||
}
|
||||
|
||||
private writeToDisk = debouncedJSONDiskWriter(100, Paths.extensionsMappingJson, () => this.mapping)
|
||||
}
|
||||
|
||||
export async function initializePackageManager(webContents: Electron.WebContents): Promise<PackageManagerInterface> {
|
||||
@@ -230,7 +224,7 @@ async function syncComponents(webContents: Electron.WebContents, mapping: Mappin
|
||||
await checkForUpdate(webContents, mapping, component)
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code === FileDoesNotExist) {
|
||||
if (error.code === FileErrorCodes.FileDoesNotExist) {
|
||||
/** We have a component but no content. Install the component */
|
||||
await installComponent(webContents, mapping, component, component.content.package_info, version)
|
||||
} else {
|
||||
@@ -289,7 +283,7 @@ async function unnestLegacyStructure(dir: string) {
|
||||
const sourceDir = path.join(dir, fileNames[0])
|
||||
const destDir = dir
|
||||
|
||||
await moveDirContents(sourceDir, destDir)
|
||||
await new FilesManager().moveDirContents(sourceDir, destDir)
|
||||
}
|
||||
|
||||
async function installComponent(
|
||||
@@ -331,13 +325,15 @@ async function installComponent(
|
||||
downloadFile(downloadUrl, paths.downloadPath),
|
||||
(async () => {
|
||||
/** Clear the component's directory before extracting the zip. */
|
||||
await ensureDirectoryExists(paths.absolutePath)
|
||||
await deleteDirContents(paths.absolutePath)
|
||||
const filesManager = new FilesManager()
|
||||
await filesManager.ensureDirectoryExists(paths.absolutePath)
|
||||
await filesManager.deleteDirContents(paths.absolutePath)
|
||||
})(),
|
||||
])
|
||||
|
||||
logMessage('Extracting', paths.downloadPath, 'to', paths.absolutePath)
|
||||
await extractZip(paths.downloadPath, paths.absolutePath)
|
||||
const filesManager = new FilesManager()
|
||||
await filesManager.extractZip(paths.downloadPath, paths.absolutePath)
|
||||
|
||||
const legacyStructure = await usesLegacyNestedFolderStructure(paths.absolutePath)
|
||||
if (legacyStructure) {
|
||||
@@ -348,7 +344,7 @@ async function installComponent(
|
||||
try {
|
||||
/** Try to read 'sn.main' field from 'package.json' file */
|
||||
const packageJsonPath = path.join(paths.absolutePath, 'package.json')
|
||||
const packageJson = await readJSONFile<{
|
||||
const packageJson = await filesManager.readJSONFile<{
|
||||
sn?: { main?: string }
|
||||
version?: string
|
||||
}>(packageJsonPath)
|
||||
@@ -403,6 +399,8 @@ async function uninstallComponent(mapping: MappingFileHandler, uuid: string) {
|
||||
/** No mapping for component */
|
||||
return
|
||||
}
|
||||
await deleteDir(path.join(Paths.userDataDir, componentMapping.location))
|
||||
mapping.remove(uuid)
|
||||
const result = await new FilesManager().deleteDir(path.join(Paths.userDataDir, componentMapping.location))
|
||||
if (!result.isFailed()) {
|
||||
mapping.remove(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ import {
|
||||
FileBackupsMapping,
|
||||
FileBackupReadToken,
|
||||
FileBackupReadChunkResponse,
|
||||
HomeServerManagerInterface,
|
||||
PlaintextBackupsMapping,
|
||||
DirectoryManagerInterface,
|
||||
} from '@web/Application/Device/DesktopSnjsExports'
|
||||
import { app, BrowserWindow } from 'electron'
|
||||
import { KeychainInterface } from '../Keychain/KeychainInterface'
|
||||
@@ -34,6 +36,8 @@ export class RemoteBridge implements CrossProcessBridge {
|
||||
private menus: MenuManagerInterface,
|
||||
private fileBackups: FileBackupsDevice,
|
||||
private media: MediaManagerInterface,
|
||||
private homeServerManager: HomeServerManagerInterface,
|
||||
private directoryManager: DirectoryManagerInterface,
|
||||
) {}
|
||||
|
||||
get exposableValue(): CrossProcessBridge {
|
||||
@@ -63,6 +67,8 @@ export class RemoteBridge implements CrossProcessBridge {
|
||||
getFileBackupReadToken: this.getFileBackupReadToken.bind(this),
|
||||
readNextChunk: this.readNextChunk.bind(this),
|
||||
askForMediaAccess: this.askForMediaAccess.bind(this),
|
||||
startHomeServer: this.startHomeServer.bind(this),
|
||||
stopHomeServer: this.stopHomeServer.bind(this),
|
||||
wasLegacyTextBackupsExplicitlyDisabled: this.wasLegacyTextBackupsExplicitlyDisabled.bind(this),
|
||||
getLegacyTextBackupsLocation: this.getLegacyTextBackupsLocation.bind(this),
|
||||
saveTextBackupData: this.saveTextBackupData.bind(this),
|
||||
@@ -70,6 +76,7 @@ export class RemoteBridge implements CrossProcessBridge {
|
||||
openLocation: this.openLocation.bind(this),
|
||||
presentDirectoryPickerForLocationChangeAndTransferOld:
|
||||
this.presentDirectoryPickerForLocationChangeAndTransferOld.bind(this),
|
||||
getDirectoryManagerLastErrorMessage: this.getDirectoryManagerLastErrorMessage.bind(this),
|
||||
getPlaintextBackupsMappingFile: this.getPlaintextBackupsMappingFile.bind(this),
|
||||
persistPlaintextBackupsMappingFile: this.persistPlaintextBackupsMappingFile.bind(this),
|
||||
getTextBackupsCount: this.getTextBackupsCount.bind(this),
|
||||
@@ -77,6 +84,14 @@ export class RemoteBridge implements CrossProcessBridge {
|
||||
getUserDocumentsDirectory: this.getUserDocumentsDirectory.bind(this),
|
||||
monitorPlaintextBackupsLocationForChanges: this.monitorPlaintextBackupsLocationForChanges.bind(this),
|
||||
joinPaths: this.joinPaths.bind(this),
|
||||
setHomeServerConfiguration: this.setHomeServerConfiguration.bind(this),
|
||||
getHomeServerConfiguration: this.getHomeServerConfiguration.bind(this),
|
||||
setHomeServerDataLocation: this.setHomeServerDataLocation.bind(this),
|
||||
activatePremiumFeatures: this.activatePremiumFeatures.bind(this),
|
||||
isHomeServerRunning: this.isHomeServerRunning.bind(this),
|
||||
getHomeServerLogs: this.getHomeServerLogs.bind(this),
|
||||
getHomeServerUrl: this.getHomeServerUrl.bind(this),
|
||||
getHomeServerLastErrorMessage: this.getHomeServerLastErrorMessage.bind(this),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,15 +216,19 @@ export class RemoteBridge implements CrossProcessBridge {
|
||||
return this.fileBackups.savePlaintextNoteBackup(location, uuid, name, tags, data)
|
||||
}
|
||||
|
||||
openLocation(path: string): Promise<void> {
|
||||
return this.fileBackups.openLocation(path)
|
||||
async openLocation(path: string): Promise<void> {
|
||||
return this.directoryManager.openLocation(path)
|
||||
}
|
||||
|
||||
presentDirectoryPickerForLocationChangeAndTransferOld(
|
||||
async presentDirectoryPickerForLocationChangeAndTransferOld(
|
||||
appendPath: string,
|
||||
oldLocation?: string | undefined,
|
||||
): Promise<string | undefined> {
|
||||
return this.fileBackups.presentDirectoryPickerForLocationChangeAndTransferOld(appendPath, oldLocation)
|
||||
return this.directoryManager.presentDirectoryPickerForLocationChangeAndTransferOld(appendPath, oldLocation)
|
||||
}
|
||||
|
||||
async getDirectoryManagerLastErrorMessage(): Promise<string | undefined> {
|
||||
return this.directoryManager.getDirectoryManagerLastErrorMessage()
|
||||
}
|
||||
|
||||
getPlaintextBackupsMappingFile(location: string): Promise<PlaintextBackupsMapping> {
|
||||
@@ -243,4 +262,44 @@ export class RemoteBridge implements CrossProcessBridge {
|
||||
askForMediaAccess(type: 'camera' | 'microphone'): Promise<boolean> {
|
||||
return this.media.askForMediaAccess(type)
|
||||
}
|
||||
|
||||
async startHomeServer(): Promise<string | undefined> {
|
||||
return this.homeServerManager.startHomeServer()
|
||||
}
|
||||
|
||||
async stopHomeServer(): Promise<string | undefined> {
|
||||
return this.homeServerManager.stopHomeServer()
|
||||
}
|
||||
|
||||
async setHomeServerConfiguration(configurationJSONString: string): Promise<void> {
|
||||
return this.homeServerManager.setHomeServerConfiguration(configurationJSONString)
|
||||
}
|
||||
|
||||
async getHomeServerConfiguration(): Promise<string | undefined> {
|
||||
return this.homeServerManager.getHomeServerConfiguration()
|
||||
}
|
||||
|
||||
async setHomeServerDataLocation(location: string): Promise<void> {
|
||||
return this.homeServerManager.setHomeServerDataLocation(location)
|
||||
}
|
||||
|
||||
async activatePremiumFeatures(username: string): Promise<string | undefined> {
|
||||
return this.homeServerManager.activatePremiumFeatures(username)
|
||||
}
|
||||
|
||||
async isHomeServerRunning(): Promise<boolean> {
|
||||
return this.homeServerManager.isHomeServerRunning()
|
||||
}
|
||||
|
||||
async getHomeServerLogs(): Promise<string[]> {
|
||||
return this.homeServerManager.getHomeServerLogs()
|
||||
}
|
||||
|
||||
async getHomeServerUrl(): Promise<string | undefined> {
|
||||
return this.homeServerManager.getHomeServerUrl()
|
||||
}
|
||||
|
||||
async getHomeServerLastErrorMessage(): Promise<string | undefined> {
|
||||
return this.homeServerManager.getHomeServerLastErrorMessage()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ export enum StoreKeys {
|
||||
UseNativeKeychain = 'useNativeKeychain',
|
||||
LastRunVersion = 'LastRunVersion',
|
||||
|
||||
HomeServerDataLocation = 'HomeServerDataLocation',
|
||||
|
||||
LegacyTextBackupsLocation = 'backupsLocation',
|
||||
LegacyTextBackupsDisabled = 'backupsDisabled',
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import fs from 'fs'
|
||||
import { Language } from '../SpellcheckerManager'
|
||||
import { FileDoesNotExist } from '../Utils/FileUtils'
|
||||
import { ensureIsBoolean, isBoolean } from '../Utils/Utils'
|
||||
import { StoreData, StoreKeys } from './StoreKeys'
|
||||
import { logError } from './Store'
|
||||
import { FileErrorCodes } from '../File/FileErrorCodes'
|
||||
|
||||
export function createSanitizedStoreData(data: any = {}): StoreData {
|
||||
return {
|
||||
@@ -69,7 +69,7 @@ export function parseDataFile(filePath: string) {
|
||||
return createSanitizedStoreData(userData)
|
||||
} catch (error: any) {
|
||||
console.log('Error reading store file', error)
|
||||
if (error.code !== FileDoesNotExist) {
|
||||
if (error.code !== FileErrorCodes.FileDoesNotExist) {
|
||||
logError(error)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,291 +0,0 @@
|
||||
import { dialog } from 'electron'
|
||||
import fs, { PathLike } from 'fs'
|
||||
import { debounce } from 'lodash'
|
||||
import path from 'path'
|
||||
import yauzl from 'yauzl'
|
||||
import { removeFromArray } from '../Utils/Utils'
|
||||
|
||||
export const FileDoesNotExist = 'ENOENT'
|
||||
export const FileAlreadyExists = 'EEXIST'
|
||||
const OperationNotPermitted = 'EPERM'
|
||||
const DeviceIsBusy = 'EBUSY'
|
||||
|
||||
export function debouncedJSONDiskWriter(durationMs: number, location: string, data: () => unknown): () => void {
|
||||
let writingToDisk = false
|
||||
return debounce(async () => {
|
||||
if (writingToDisk) {
|
||||
return
|
||||
}
|
||||
writingToDisk = true
|
||||
try {
|
||||
await writeJSONFile(location, data())
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
writingToDisk = false
|
||||
}
|
||||
}, durationMs)
|
||||
}
|
||||
|
||||
export async function openDirectoryPicker(buttonLabel?: string): Promise<string | undefined> {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openDirectory', 'showHiddenFiles', 'createDirectory'],
|
||||
buttonLabel: buttonLabel,
|
||||
})
|
||||
|
||||
return result.filePaths[0]
|
||||
}
|
||||
|
||||
export async function readJSONFile<T>(filepath: string): Promise<T | undefined> {
|
||||
try {
|
||||
const data = await fs.promises.readFile(filepath, 'utf8')
|
||||
return JSON.parse(data)
|
||||
} catch (error) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function readJSONFileSync<T>(filepath: string): T {
|
||||
const data = fs.readFileSync(filepath, 'utf8')
|
||||
return JSON.parse(data)
|
||||
}
|
||||
|
||||
export async function writeJSONFile(filepath: string, data: unknown): Promise<void> {
|
||||
await ensureDirectoryExists(path.dirname(filepath))
|
||||
await fs.promises.writeFile(filepath, JSON.stringify(data, null, 2), 'utf8')
|
||||
}
|
||||
|
||||
export async function writeFile(filepath: string, data: string): Promise<void> {
|
||||
await ensureDirectoryExists(path.dirname(filepath))
|
||||
await fs.promises.writeFile(filepath, data, 'utf8')
|
||||
}
|
||||
|
||||
export function writeJSONFileSync(filepath: string, data: unknown): void {
|
||||
fs.writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf8')
|
||||
}
|
||||
|
||||
/** Creates the directory if it doesn't exist. */
|
||||
export async function ensureDirectoryExists(dirPath: string): Promise<void> {
|
||||
try {
|
||||
const stat = await fs.promises.lstat(dirPath)
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error('Tried to create a directory where a file of the same ' + `name already exists: ${dirPath}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code === FileDoesNotExist) {
|
||||
/**
|
||||
* No directory here. Make sure there is a *parent* directory, and then
|
||||
* create it.
|
||||
*/
|
||||
await ensureDirectoryExists(path.dirname(dirPath))
|
||||
|
||||
/** Now that its parent(s) exist, create the directory */
|
||||
try {
|
||||
await fs.promises.mkdir(dirPath)
|
||||
} catch (error: any) {
|
||||
if (error.code === FileAlreadyExists) {
|
||||
/**
|
||||
* A concurrent process must have created the directory already.
|
||||
* Make sure it *is* a directory and not something else.
|
||||
*/
|
||||
await ensureDirectoryExists(dirPath)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a directory (handling recursion.)
|
||||
* @param {string} dirPath the path of the directory
|
||||
*/
|
||||
export async function deleteDir(dirPath: string): Promise<void> {
|
||||
try {
|
||||
await deleteDirContents(dirPath)
|
||||
} catch (error: any) {
|
||||
if (error.code === FileDoesNotExist) {
|
||||
/** Directory has already been deleted. */
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
await fs.promises.rmdir(dirPath)
|
||||
}
|
||||
|
||||
export async function deleteDirContents(dirPath: string): Promise<void> {
|
||||
/**
|
||||
* Scan the directory up to ten times, to handle cases where files are being added while
|
||||
* the directory's contents are being deleted
|
||||
*/
|
||||
for (let i = 1, maxTries = 10; i < maxTries; i++) {
|
||||
const children = await fs.promises.readdir(dirPath, {
|
||||
withFileTypes: true,
|
||||
})
|
||||
|
||||
if (children.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
for (const child of children) {
|
||||
const childPath = path.join(dirPath, child.name)
|
||||
if (child.isDirectory()) {
|
||||
await deleteDirContents(childPath)
|
||||
try {
|
||||
await fs.promises.rmdir(childPath)
|
||||
} catch (error) {
|
||||
if (error !== FileDoesNotExist) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await deleteFile(childPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isChildOfDir(parent: string, potentialChild: string) {
|
||||
const relative = path.relative(parent, potentialChild)
|
||||
return relative && !relative.startsWith('..') && !path.isAbsolute(relative)
|
||||
}
|
||||
|
||||
export async function moveDirContents(srcDir: string, destDir: string): Promise<void> {
|
||||
let fileNames: string[]
|
||||
try {
|
||||
fileNames = await fs.promises.readdir(srcDir)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return
|
||||
}
|
||||
await ensureDirectoryExists(destDir)
|
||||
|
||||
if (isChildOfDir(srcDir, destDir)) {
|
||||
fileNames = fileNames.filter((name) => {
|
||||
return !isChildOfDir(destDir, path.join(srcDir, name))
|
||||
})
|
||||
removeFromArray(fileNames, path.basename(destDir))
|
||||
}
|
||||
|
||||
try {
|
||||
await moveFiles(
|
||||
fileNames.map((fileName) => path.join(srcDir, fileName)),
|
||||
destDir,
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function extractZip(source: string, dest: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
yauzl.open(source, { lazyEntries: true, autoClose: true }, (err, zipFile) => {
|
||||
let cancelled = false
|
||||
|
||||
const tryReject = (err: Error) => {
|
||||
if (!cancelled) {
|
||||
cancelled = true
|
||||
reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return tryReject(err)
|
||||
}
|
||||
|
||||
if (!zipFile) {
|
||||
return tryReject(new Error('zipFile === undefined'))
|
||||
}
|
||||
|
||||
zipFile.readEntry()
|
||||
|
||||
zipFile.on('close', resolve)
|
||||
|
||||
zipFile.on('entry', (entry) => {
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
const isEntryDirectory = entry.fileName.endsWith('/')
|
||||
if (isEntryDirectory) {
|
||||
zipFile.readEntry()
|
||||
return
|
||||
}
|
||||
|
||||
zipFile.openReadStream(entry, async (err, stream) => {
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return tryReject(err)
|
||||
}
|
||||
|
||||
if (!stream) {
|
||||
return tryReject(new Error('stream === undefined'))
|
||||
}
|
||||
|
||||
stream.on('error', tryReject)
|
||||
|
||||
const filepath = path.join(dest, entry.fileName)
|
||||
|
||||
try {
|
||||
await ensureDirectoryExists(path.dirname(filepath))
|
||||
} catch (error: any) {
|
||||
return tryReject(error)
|
||||
}
|
||||
const writeStream = fs.createWriteStream(filepath).on('error', tryReject).on('error', tryReject)
|
||||
|
||||
stream.pipe(writeStream).on('close', () => {
|
||||
zipFile.readEntry()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function moveFiles(sources: string[], destDir: string): Promise<void[]> {
|
||||
await ensureDirectoryExists(destDir)
|
||||
return Promise.all(sources.map((fileName) => moveFile(fileName, path.join(destDir, path.basename(fileName)))))
|
||||
}
|
||||
|
||||
export async function moveFile(source: PathLike, destination: PathLike) {
|
||||
try {
|
||||
await fs.promises.rename(source, destination)
|
||||
} catch (_error) {
|
||||
/** Fall back to copying and then deleting. */
|
||||
await fs.promises.copyFile(source, destination, fs.constants.COPYFILE_FICLONE_FORCE)
|
||||
await fs.promises.unlink(source)
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteFileIfExists(filePath: PathLike): Promise<void> {
|
||||
try {
|
||||
await deleteFile(filePath)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/** Deletes a file, handling EPERM and EBUSY errors on Windows. */
|
||||
export async function deleteFile(filePath: PathLike): Promise<void> {
|
||||
for (let i = 1, maxTries = 10; i < maxTries; i++) {
|
||||
try {
|
||||
await fs.promises.unlink(filePath)
|
||||
break
|
||||
} catch (error: any) {
|
||||
if (error.code === OperationNotPermitted || error.code === DeviceIsBusy) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
continue
|
||||
} else if (error.code === FileDoesNotExist) {
|
||||
/** Already deleted */
|
||||
break
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,9 @@ import { checkForUpdate, setupUpdates } from './UpdateManager'
|
||||
import { handleTestMessage, send } from './Utils/Testing'
|
||||
import { isTesting, lowercaseDriveLetter } from './Utils/Utils'
|
||||
import { initializeZoomManager } from './ZoomManager'
|
||||
import { HomeServerManager } from './HomeServer/HomeServerManager'
|
||||
import { FilesManager } from './File/FilesManager'
|
||||
import { DirectoryManager } from './Directory/DirectoryManager'
|
||||
|
||||
const WINDOW_DEFAULT_WIDTH = 1100
|
||||
const WINDOW_DEFAULT_HEIGHT = 800
|
||||
@@ -58,6 +61,7 @@ export async function createWindowState({
|
||||
const services = await createWindowServices(window, appState, appLocale)
|
||||
|
||||
require('@electron/remote/main').enable(window.webContents)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(global as any).RemoteBridge = new RemoteBridge(
|
||||
window,
|
||||
Keychain,
|
||||
@@ -72,6 +76,8 @@ export async function createWindowState({
|
||||
services.menuManager,
|
||||
services.fileBackupsManager,
|
||||
services.mediaManager,
|
||||
services.homeServerManager,
|
||||
services.directoryManager,
|
||||
)
|
||||
|
||||
const shouldOpenUrl = (url: string) => url.startsWith('http') || url.startsWith('mailto')
|
||||
@@ -201,6 +207,11 @@ async function createWindowServices(window: Electron.BrowserWindow, appState: Ap
|
||||
const spellcheckerManager = createSpellcheckerManager(appState.store, window.webContents, appLocale)
|
||||
const mediaManager = new MediaManager()
|
||||
|
||||
const filesManager = new FilesManager()
|
||||
const directoryManager = new DirectoryManager(filesManager)
|
||||
|
||||
const homeServerManager = new HomeServerManager(window.webContents, filesManager)
|
||||
|
||||
if (isTesting()) {
|
||||
handleTestMessage(MessageType.SpellCheckerManager, () => spellcheckerManager)
|
||||
}
|
||||
@@ -213,7 +224,7 @@ async function createWindowServices(window: Electron.BrowserWindow, appState: Ap
|
||||
spellcheckerManager,
|
||||
})
|
||||
|
||||
const fileBackupsManager = new FilesBackupManager(appState, window.webContents)
|
||||
const fileBackupsManager = new FilesBackupManager(appState, window.webContents, filesManager)
|
||||
|
||||
return {
|
||||
updateManager,
|
||||
@@ -224,6 +235,8 @@ async function createWindowServices(window: Electron.BrowserWindow, appState: Ap
|
||||
searchManager,
|
||||
fileBackupsManager,
|
||||
mediaManager,
|
||||
homeServerManager,
|
||||
directoryManager,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { FileBackupsDevice } from '@web/Application/Device/DesktopSnjsExports'
|
||||
import {
|
||||
DirectoryManagerInterface,
|
||||
FileBackupsDevice,
|
||||
HomeServerManagerInterface,
|
||||
} from '@web/Application/Device/DesktopSnjsExports'
|
||||
import { Component } from '../Main/Packages/PackageManagerInterface'
|
||||
|
||||
export interface CrossProcessBridge extends FileBackupsDevice {
|
||||
export interface CrossProcessBridge extends FileBackupsDevice, DirectoryManagerInterface, HomeServerManagerInterface {
|
||||
get extServerHost(): string
|
||||
get useNativeKeychain(): boolean
|
||||
get rendererPath(): string
|
||||
|
||||
@@ -25,6 +25,46 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
|
||||
super(appVersion)
|
||||
}
|
||||
|
||||
async getHomeServerUrl(): Promise<string | undefined> {
|
||||
return this.remoteBridge.getHomeServerUrl()
|
||||
}
|
||||
|
||||
async getHomeServerLastErrorMessage(): Promise<string | undefined> {
|
||||
return this.remoteBridge.getHomeServerLastErrorMessage()
|
||||
}
|
||||
|
||||
async isHomeServerRunning(): Promise<boolean> {
|
||||
return this.remoteBridge.isHomeServerRunning()
|
||||
}
|
||||
|
||||
async activatePremiumFeatures(username: string): Promise<string | undefined> {
|
||||
return this.remoteBridge.activatePremiumFeatures(username)
|
||||
}
|
||||
|
||||
async setHomeServerConfiguration(configurationJSONString: string): Promise<void> {
|
||||
return this.remoteBridge.setHomeServerConfiguration(configurationJSONString)
|
||||
}
|
||||
|
||||
async getHomeServerConfiguration(): Promise<string | undefined> {
|
||||
return this.remoteBridge.getHomeServerConfiguration()
|
||||
}
|
||||
|
||||
async setHomeServerDataLocation(location: string): Promise<void> {
|
||||
return this.remoteBridge.setHomeServerDataLocation(location)
|
||||
}
|
||||
|
||||
startHomeServer(): Promise<string | undefined> {
|
||||
return this.remoteBridge.startHomeServer()
|
||||
}
|
||||
|
||||
stopHomeServer(): Promise<string | undefined> {
|
||||
return this.remoteBridge.stopHomeServer()
|
||||
}
|
||||
|
||||
getHomeServerLogs(): Promise<string[]> {
|
||||
return this.remoteBridge.getHomeServerLogs()
|
||||
}
|
||||
|
||||
openLocation(path: string): Promise<void> {
|
||||
return this.remoteBridge.openLocation(path)
|
||||
}
|
||||
@@ -36,6 +76,10 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
|
||||
return this.remoteBridge.presentDirectoryPickerForLocationChangeAndTransferOld(appendPath, oldLocation)
|
||||
}
|
||||
|
||||
getDirectoryManagerLastErrorMessage(): Promise<string | undefined> {
|
||||
return this.remoteBridge.getDirectoryManagerLastErrorMessage()
|
||||
}
|
||||
|
||||
getFilesBackupsMappingFile(location: string): Promise<FileBackupsMapping> {
|
||||
return this.remoteBridge.getFilesBackupsMappingFile(location)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ process.once('loaded', function () {
|
||||
|
||||
setInstallComponentCompleteHandler: (handler: MainEventHandler) =>
|
||||
ipcRenderer.on(MessageToWebApp.InstallComponentComplete, handler),
|
||||
|
||||
setHomeServerStartedHandler: (handler: MainEventHandler) =>
|
||||
ipcRenderer.on(MessageToWebApp.HomeServerStarted, handler),
|
||||
|
||||
setConsoleLogHandler: (handler: MainEventHandler) => ipcRenderer.on(MessageToWebApp.ConsoleLog, handler),
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld('electronMainEvents', mainEvents)
|
||||
|
||||
@@ -54,6 +54,12 @@ window.onload = () => {
|
||||
void loadAndStartApplication()
|
||||
}
|
||||
|
||||
window.onunload = () => {
|
||||
if (window.device) {
|
||||
void window.device.stopHomeServer()
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns whether the keychain structure is up to date or not */
|
||||
async function migrateKeychain(remoteBridge: CrossProcessBridge): Promise<boolean> {
|
||||
if (!remoteBridge.useNativeKeychain) {
|
||||
@@ -122,7 +128,7 @@ async function configureWindow(remoteBridge: CrossProcessBridge) {
|
||||
/* Use custom title bar. Take the sn-titlebar-height off of
|
||||
the app content height so its not overflowing */
|
||||
sheet.insertRule(
|
||||
'[role="dialog"] { position: relative; height: calc(100vh - var(--sn-desktop-titlebar-height)) !important; margin-top: var(--sn-desktop-titlebar-height) !important; }',
|
||||
'[role="dialog"] { height: calc(100vh - var(--sn-desktop-titlebar-height)) !important; top: var(--sn-desktop-titlebar-height); }',
|
||||
sheet.cssRules.length,
|
||||
)
|
||||
sheet.insertRule(
|
||||
@@ -144,10 +150,19 @@ window.electronMainEvents.setWindowFocusedHandler(() => {
|
||||
window.webClient.windowGainedFocus()
|
||||
})
|
||||
|
||||
window.electronMainEvents.setInstallComponentCompleteHandler((_: IpcRendererEvent, data: any) => {
|
||||
void window.webClient.onComponentInstallationComplete(data.component, undefined)
|
||||
window.electronMainEvents.setConsoleLogHandler((_: IpcRendererEvent, message: unknown) => {
|
||||
window.webClient.consoleLog(message as string)
|
||||
})
|
||||
|
||||
window.electronMainEvents.setInstallComponentCompleteHandler((_: IpcRendererEvent, data: unknown) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
void window.webClient.onComponentInstallationComplete((data as any).component, undefined)
|
||||
})
|
||||
|
||||
window.electronMainEvents.setWatchedDirectoriesChangeHandler((_: IpcRendererEvent, changes: unknown) => {
|
||||
void window.webClient.handleWatchedDirectoriesChanges(changes as DesktopWatchedDirectoriesChanges)
|
||||
})
|
||||
|
||||
window.electronMainEvents.setHomeServerStartedHandler((_: IpcRendererEvent, serverUrl: unknown) => {
|
||||
void window.webClient.handleHomeServerStarted(serverUrl as string)
|
||||
})
|
||||
|
||||
@@ -8,4 +8,6 @@ export interface ElectronMainEvents {
|
||||
setWindowFocusedHandler(handler: MainEventHandler): void
|
||||
setInstallComponentCompleteHandler(handler: MainEventHandler): void
|
||||
setWatchedDirectoriesChangeHandler(handler: MainEventHandler): void
|
||||
setHomeServerStartedHandler(handler: MainEventHandler): void
|
||||
setConsoleLogHandler(handler: MainEventHandler): void
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ export enum MessageToWebApp {
|
||||
WindowFocused = 'window-focused',
|
||||
InstallComponentComplete = 'install-component-complete',
|
||||
WatchedDirectoriesChanges = 'watched-directories-changes',
|
||||
HomeServerStarted = 'home-server-started',
|
||||
ConsoleLog = 'console-log',
|
||||
}
|
||||
|
||||
export enum MessageToMainProcess {
|
||||
|
||||
@@ -67,6 +67,7 @@ module.exports = function ({ onlyTranspileTypescript = false, experimentalFeatur
|
||||
module: moduleConfig,
|
||||
externals: {
|
||||
keytar: 'commonjs keytar',
|
||||
"@standardnotes/home-server": "commonjs @standardnotes/home-server",
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/desktop",
|
||||
"main": "./app/dist/index.js",
|
||||
"version": "3.106.67",
|
||||
"version": "3.108.15",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
@@ -35,6 +35,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "^2.0.9",
|
||||
"@standardnotes/domain-core": "^1.22.0",
|
||||
"@standardnotes/electron-clear-data": "1.1.1",
|
||||
"@standardnotes/web": "workspace:*",
|
||||
"axios": "^1.1.3",
|
||||
@@ -44,7 +45,7 @@
|
||||
"electron": "22.1.0",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-updater": "^5.3.0",
|
||||
"fs-extra": "^10.1.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"mobx": "^6.7.0"
|
||||
@@ -52,6 +53,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "*",
|
||||
"@babel/preset-env": "*",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/lodash": "^4.14.189",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/node": "18",
|
||||
@@ -107,7 +109,8 @@
|
||||
"NSCameraUsageDescription": "Standard Notes requires access to your camera to enable the Moments feature."
|
||||
},
|
||||
"asarUnpack": [
|
||||
"node_modules/keytar"
|
||||
"node_modules/keytar",
|
||||
"node_modules/@standardnotes/home-server"
|
||||
],
|
||||
"target": [
|
||||
"dmg",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
"moduleResolution": "node",
|
||||
"module": "es2020",
|
||||
"types": ["node"],
|
||||
"allowJs": true,
|
||||
"noEmit": false,
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.21.49](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.21.48](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.21.47](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.21.46](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.21.45](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.21.44](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-06-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/encryption",
|
||||
"version": "1.21.44",
|
||||
"version": "1.21.49",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -28,7 +28,8 @@
|
||||
"typescript": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.48.3",
|
||||
"@standardnotes/common": "^1.50.0",
|
||||
"@standardnotes/domain-core": "^1.22.0",
|
||||
"@standardnotes/models": "workspace:*",
|
||||
"@standardnotes/responses": "workspace:*",
|
||||
"@standardnotes/sncrypto-common": "workspace:*",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import {
|
||||
ConflictStrategy,
|
||||
DecryptedItem,
|
||||
@@ -10,7 +11,7 @@ import {
|
||||
} from '@standardnotes/models'
|
||||
|
||||
export function isItemsKey(x: unknown): x is ItemsKeyInterface {
|
||||
return (x as ItemsKeyInterface).content_type === ContentType.ItemsKey
|
||||
return (x as ItemsKeyInterface).content_type === ContentType.TYPES.ItemsKey
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { DecryptedItemMutator, ItemsKeyContent, RegisterItemClass } from '@standardnotes/models'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { SNItemsKey } from './ItemsKey'
|
||||
import { ItemsKeyMutator } from './ItemsKeyMutator'
|
||||
|
||||
RegisterItemClass(ContentType.ItemsKey, SNItemsKey, ItemsKeyMutator as unknown as DecryptedItemMutator<ItemsKeyContent>)
|
||||
RegisterItemClass(
|
||||
ContentType.TYPES.ItemsKey,
|
||||
SNItemsKey,
|
||||
ItemsKeyMutator as unknown as DecryptedItemMutator<ItemsKeyContent>,
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import {
|
||||
ConflictStrategy,
|
||||
DecryptedItem,
|
||||
@@ -10,7 +11,7 @@ import {
|
||||
} from '@standardnotes/models'
|
||||
|
||||
export function isKeySystemItemsKey(x: unknown): x is KeySystemItemsKeyInterface {
|
||||
return (x as KeySystemItemsKeyInterface).content_type === ContentType.KeySystemItemsKey
|
||||
return (x as KeySystemItemsKeyInterface).content_type === ContentType.TYPES.KeySystemItemsKey
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { DecryptedItemMutator, KeySystemItemsKeyContent, RegisterItemClass } from '@standardnotes/models'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
import { KeySystemItemsKey } from './KeySystemItemsKey'
|
||||
import { KeySystemItemsKeyMutator } from './KeySystemItemsKeyMutator'
|
||||
|
||||
RegisterItemClass(
|
||||
ContentType.KeySystemItemsKey,
|
||||
ContentType.TYPES.KeySystemItemsKey,
|
||||
KeySystemItemsKey,
|
||||
KeySystemItemsKeyMutator as unknown as DecryptedItemMutator<KeySystemItemsKeyContent>,
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import {
|
||||
DecryptedPayload,
|
||||
FillItemContentSpecialized,
|
||||
@@ -9,13 +9,14 @@ import {
|
||||
} from '@standardnotes/models'
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
import { SNRootKey } from './RootKey'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
export function CreateNewRootKey<K extends RootKeyInterface>(content: RootKeyContentSpecialized): K {
|
||||
const uuid = UuidGenerator.GenerateUuid()
|
||||
|
||||
const payload = new DecryptedPayload<RootKeyContent>({
|
||||
uuid: uuid,
|
||||
content_type: ContentType.RootKey,
|
||||
content_type: ContentType.TYPES.RootKey,
|
||||
content: FillRootKeyContent(content),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ContentType, KeyParamsOrigination, ProtocolVersion, ProtocolVersionLength } from '@standardnotes/common'
|
||||
import { KeyParamsOrigination, ProtocolVersion, ProtocolVersionLength } from '@standardnotes/common'
|
||||
import {
|
||||
CreateDecryptedItemFromPayload,
|
||||
DecryptedPayload,
|
||||
@@ -32,6 +32,7 @@ import { PublicKeySet } from '../Types/PublicKeySet'
|
||||
import { AsymmetricDecryptResult } from '../Types/AsymmetricDecryptResult'
|
||||
import { AsymmetricSignatureVerificationDetachedResult } from '../Types/AsymmetricSignatureVerificationDetachedResult'
|
||||
import { AsyncOperatorInterface } from '../OperatorInterface/AsyncOperatorInterface'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
const NO_IV = '00000000000000000000000000000000'
|
||||
|
||||
@@ -71,7 +72,7 @@ export class SNProtocolOperator001 implements OperatorInterface, AsyncOperatorIn
|
||||
public createItemsKey(): ItemsKeyInterface {
|
||||
const payload = new DecryptedPayload({
|
||||
uuid: UuidGenerator.GenerateUuid(),
|
||||
content_type: ContentType.ItemsKey,
|
||||
content_type: ContentType.TYPES.ItemsKey,
|
||||
content: this.generateNewItemsKeyContent(),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as Common from '@standardnotes/common'
|
||||
import * as Models from '@standardnotes/models'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
import { ItemContent, PayloadTimestampDefaults } from '@standardnotes/models'
|
||||
import * as Utils from '@standardnotes/utils'
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
@@ -44,7 +45,7 @@ export class SNProtocolOperator002 extends SNProtocolOperator001 {
|
||||
public override createItemsKey(): Models.ItemsKeyInterface {
|
||||
const payload = new Models.DecryptedPayload({
|
||||
uuid: UuidGenerator.GenerateUuid(),
|
||||
content_type: Common.ContentType.ItemsKey,
|
||||
content_type: ContentType.TYPES.ItemsKey,
|
||||
content: this.generateNewItemsKeyContent(),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ContentType, KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
|
||||
import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
|
||||
import {
|
||||
CreateDecryptedItemFromPayload,
|
||||
DecryptedPayload,
|
||||
@@ -14,6 +14,7 @@ import { CreateNewRootKey } from '../../Keys/RootKey/Functions'
|
||||
import { Create003KeyParams } from '../../Keys/RootKey/KeyParamsFunctions'
|
||||
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
|
||||
import { SNProtocolOperator002 } from '../002/Operator002'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
/**
|
||||
* @legacy
|
||||
@@ -46,7 +47,7 @@ export class SNProtocolOperator003 extends SNProtocolOperator002 {
|
||||
const content = this.generateNewItemsKeyContent()
|
||||
const payload = new DecryptedPayload({
|
||||
uuid: UuidGenerator.GenerateUuid(),
|
||||
content_type: ContentType.ItemsKey,
|
||||
content_type: ContentType.TYPES.ItemsKey,
|
||||
content: FillItemContent(content),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { DecryptedPayload, ItemContent, ItemsKeyContent, PayloadTimestampDefaults } from '@standardnotes/models'
|
||||
import { SNItemsKey } from '../../Keys/ItemsKey/ItemsKey'
|
||||
import { SNProtocolOperator004 } from './Operator004'
|
||||
import { getMockedCrypto } from './MockedCrypto'
|
||||
import { deconstructEncryptedPayloadString } from './V004AlgorithmHelpers'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
describe('operator 004', () => {
|
||||
const crypto = getMockedCrypto()
|
||||
@@ -31,7 +32,7 @@ describe('operator 004', () => {
|
||||
it('should generateEncryptedParameters', () => {
|
||||
const payload = {
|
||||
uuid: '123',
|
||||
content_type: ContentType.Note,
|
||||
content_type: ContentType.TYPES.Note,
|
||||
content: { foo: 'bar' } as unknown as jest.Mocked<ItemContent>,
|
||||
...PayloadTimestampDefaults(),
|
||||
} as jest.Mocked<DecryptedPayload>
|
||||
@@ -39,7 +40,7 @@ describe('operator 004', () => {
|
||||
const key = new SNItemsKey(
|
||||
new DecryptedPayload<ItemsKeyContent>({
|
||||
uuid: 'key-456',
|
||||
content_type: ContentType.ItemsKey,
|
||||
content_type: ContentType.TYPES.ItemsKey,
|
||||
content: {
|
||||
itemsKey: 'secret',
|
||||
version: ProtocolVersion.V004,
|
||||
@@ -56,7 +57,7 @@ describe('operator 004', () => {
|
||||
key_system_identifier: undefined,
|
||||
shared_vault_uuid: undefined,
|
||||
content: '004:random-string:<e>{"foo"|"bar"}<e>:base64-{"u"|"123","v"|"004"}:base64-{}',
|
||||
content_type: ContentType.Note,
|
||||
content_type: ContentType.TYPES.Note,
|
||||
enc_item_key: '004:random-string:<e>random-string<e>:base64-{"u"|"123","v"|"004"}:base64-{}',
|
||||
version: '004',
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
RootKeyInterface,
|
||||
KeySystemRootKeyParamsInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ContentType, KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
|
||||
import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
|
||||
import { HexString, PkcKeyPair, PureCryptoInterface, Utf8String } from '@standardnotes/sncrypto-common'
|
||||
import { V004Algorithm } from '../../Algorithm'
|
||||
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
|
||||
@@ -48,6 +48,7 @@ import { AsymmetricSignatureVerificationDetachedResult } from '../Types/Asymmetr
|
||||
import { AsymmetricSignatureVerificationDetachedUseCase } from './UseCase/Asymmetric/AsymmetricSignatureVerificationDetached'
|
||||
import { DeriveKeySystemRootKeyUseCase } from './UseCase/KeySystem/DeriveKeySystemRootKey'
|
||||
import { SyncOperatorInterface } from '../OperatorInterface/SyncOperatorInterface'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
export class SNProtocolOperator004 implements OperatorInterface, SyncOperatorInterface {
|
||||
constructor(protected readonly crypto: PureCryptoInterface) {}
|
||||
@@ -76,7 +77,7 @@ export class SNProtocolOperator004 implements OperatorInterface, SyncOperatorInt
|
||||
public createItemsKey(): ItemsKeyInterface {
|
||||
const payload = new DecryptedPayload({
|
||||
uuid: UuidGenerator.GenerateUuid(),
|
||||
content_type: ContentType.ItemsKey,
|
||||
content_type: ContentType.TYPES.ItemsKey,
|
||||
content: this.generateNewItemsKeyContent(),
|
||||
key_system_identifier: undefined,
|
||||
shared_vault_uuid: undefined,
|
||||
|
||||
+3
-2
@@ -10,7 +10,8 @@ import {
|
||||
} from '@standardnotes/models'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { V004Algorithm } from '../../../../Algorithm'
|
||||
import { ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
export class CreateKeySystemItemsKeyUseCase {
|
||||
constructor(private readonly crypto: PureCryptoInterface) {}
|
||||
@@ -31,7 +32,7 @@ export class CreateKeySystemItemsKeyUseCase {
|
||||
|
||||
const transferPayload: DecryptedTransferPayload = {
|
||||
uuid: dto.uuid,
|
||||
content_type: ContentType.KeySystemItemsKey,
|
||||
content_type: ContentType.TYPES.KeySystemItemsKey,
|
||||
key_system_identifier: dto.keySystemIdentifier,
|
||||
shared_vault_uuid: dto.sharedVaultUuid,
|
||||
content: content,
|
||||
|
||||
+3
-2
@@ -12,7 +12,8 @@ import {
|
||||
PayloadTimestampDefaults,
|
||||
KeySystemRootKeyParamsInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
export class DeriveKeySystemRootKeyUseCase {
|
||||
constructor(private readonly crypto: PureCryptoInterface) {}
|
||||
@@ -44,7 +45,7 @@ export class DeriveKeySystemRootKeyUseCase {
|
||||
|
||||
const payload = new DecryptedPayload<KeySystemRootKeyContent>({
|
||||
uuid: uuid,
|
||||
content_type: ContentType.KeySystemRootKey,
|
||||
content_type: ContentType.TYPES.KeySystemRootKey,
|
||||
content: FillItemContentSpecialized(content),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
|
||||
+9
-8
@@ -1,5 +1,5 @@
|
||||
import { CreateAnyKeyParams } from '../../../../Keys/RootKey/KeyParamsFunctions'
|
||||
import { AnyKeyParamsContent, ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||
import { AnyKeyParamsContent, ProtocolVersion } from '@standardnotes/common'
|
||||
import { GenerateAuthenticatedDataUseCase } from './GenerateAuthenticatedData'
|
||||
import {
|
||||
DecryptedPayloadInterface,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
RootKeyInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { KeySystemItemsKey } from '../../../../Keys/KeySystemItemsKey/KeySystemItemsKey'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
describe('generate authenticated data use case', () => {
|
||||
let usecase: GenerateAuthenticatedDataUseCase
|
||||
@@ -19,7 +20,7 @@ describe('generate authenticated data use case', () => {
|
||||
it('should include key params if payload being encrypted is an items key', () => {
|
||||
const payload = {
|
||||
uuid: '123',
|
||||
content_type: ContentType.ItemsKey,
|
||||
content_type: ContentType.TYPES.ItemsKey,
|
||||
} as jest.Mocked<DecryptedPayloadInterface>
|
||||
|
||||
const keyParams = CreateAnyKeyParams({
|
||||
@@ -42,7 +43,7 @@ describe('generate authenticated data use case', () => {
|
||||
it('should include root key params if payload is a key system items key', () => {
|
||||
const payload = {
|
||||
uuid: '123',
|
||||
content_type: ContentType.KeySystemItemsKey,
|
||||
content_type: ContentType.TYPES.KeySystemItemsKey,
|
||||
shared_vault_uuid: 'shared-vault-uuid-123',
|
||||
key_system_identifier: 'key-system-identifier-123',
|
||||
} as jest.Mocked<DecryptedPayloadInterface>
|
||||
@@ -52,7 +53,7 @@ describe('generate authenticated data use case', () => {
|
||||
keyParams: {
|
||||
seed: 'seed-123',
|
||||
},
|
||||
content_type: ContentType.KeySystemRootKey,
|
||||
content_type: ContentType.TYPES.KeySystemRootKey,
|
||||
token: '123',
|
||||
} as jest.Mocked<KeySystemRootKeyInterface>
|
||||
|
||||
@@ -70,7 +71,7 @@ describe('generate authenticated data use case', () => {
|
||||
it('should include key system identifier and shared vault uuid', () => {
|
||||
const payload = {
|
||||
uuid: '123',
|
||||
content_type: ContentType.Note,
|
||||
content_type: ContentType.TYPES.Note,
|
||||
shared_vault_uuid: 'shared-vault-uuid-123',
|
||||
key_system_identifier: 'key-system-identifier-123',
|
||||
} as jest.Mocked<DecryptedPayloadInterface>
|
||||
@@ -78,7 +79,7 @@ describe('generate authenticated data use case', () => {
|
||||
const itemsKey = {
|
||||
creationTimestamp: 123,
|
||||
keyVersion: ProtocolVersion.V004,
|
||||
content_type: ContentType.KeySystemItemsKey,
|
||||
content_type: ContentType.TYPES.KeySystemItemsKey,
|
||||
} as jest.Mocked<KeySystemItemsKey>
|
||||
|
||||
const authenticatedData = usecase.execute(payload, itemsKey)
|
||||
@@ -94,11 +95,11 @@ describe('generate authenticated data use case', () => {
|
||||
it('should include only uuid and version if non-keysystem item with items key', () => {
|
||||
const payload = {
|
||||
uuid: '123',
|
||||
content_type: ContentType.Note,
|
||||
content_type: ContentType.TYPES.Note,
|
||||
} as jest.Mocked<DecryptedPayloadInterface>
|
||||
|
||||
const itemsKey = {
|
||||
content_type: ContentType.ItemsKey,
|
||||
content_type: ContentType.TYPES.ItemsKey,
|
||||
} as jest.Mocked<ItemsKeyInterface>
|
||||
|
||||
const authenticatedData = usecase.execute(payload, itemsKey)
|
||||
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { getMockedCrypto } from '../../MockedCrypto'
|
||||
import { GenerateDecryptedParametersUseCase } from './GenerateDecryptedParameters'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { DecryptedPayloadInterface, ItemsKeyInterface } from '@standardnotes/models'
|
||||
import { GenerateEncryptedParametersUseCase } from './GenerateEncryptedParameters'
|
||||
import { EncryptedInputParameters, EncryptedOutputParameters } from '../../../../Types/EncryptedParameters'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
describe('generate decrypted parameters usecase', () => {
|
||||
let crypto: PureCryptoInterface
|
||||
@@ -18,7 +18,7 @@ describe('generate decrypted parameters usecase', () => {
|
||||
itemsKey = {
|
||||
uuid: 'items-key-id',
|
||||
itemsKey: 'items-key',
|
||||
content_type: ContentType.ItemsKey,
|
||||
content_type: ContentType.TYPES.ItemsKey,
|
||||
} as jest.Mocked<ItemsKeyInterface>
|
||||
})
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('generate decrypted parameters usecase', () => {
|
||||
content: {
|
||||
text: plaintext,
|
||||
},
|
||||
content_type: ContentType.Note,
|
||||
content_type: ContentType.TYPES.Note,
|
||||
} as unknown as jest.Mocked<DecryptedPayloadInterface>
|
||||
|
||||
const encryptedParametersUsecase = new GenerateEncryptedParametersUseCase(crypto)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user