Merge pull request #1646 from omnivore-app/fix/import-matter-content
Update matter import handler to use archives instead of just history files
This commit is contained in:
@ -18,7 +18,7 @@ import { v4 as uuidv4 } from 'uuid'
|
||||
import { buildLogger } from '../../utils/logger'
|
||||
|
||||
const MAX_DAILY_UPLOADS = 4
|
||||
const VALID_CONTENT_TYPES = ['text/csv']
|
||||
const VALID_CONTENT_TYPES = ['text/csv', 'application/zip']
|
||||
|
||||
const logger = buildLogger('app.dispatch')
|
||||
|
||||
@ -26,6 +26,8 @@ const extensionForContentType = (contentType: string) => {
|
||||
switch (contentType) {
|
||||
case 'text/csv':
|
||||
return 'csv'
|
||||
case 'application/zip':
|
||||
return 'zip'
|
||||
}
|
||||
return '.unknown'
|
||||
}
|
||||
|
||||
31
packages/import-handler/Dockerfile
Normal file
31
packages/import-handler/Dockerfile
Normal file
@ -0,0 +1,31 @@
|
||||
FROM node:14.18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
|
||||
RUN apk add g++ make python3
|
||||
|
||||
ENV PORT 8080
|
||||
|
||||
COPY package.json .
|
||||
COPY yarn.lock .
|
||||
COPY tsconfig.json .
|
||||
COPY .eslintrc .
|
||||
|
||||
COPY /packages/readabilityjs/package.json ./packages/readabilityjs/package.json
|
||||
COPY /packages/import-handler/package.json ./packages/import-handler/package.json
|
||||
|
||||
RUN yarn install --pure-lockfile
|
||||
|
||||
ADD /packages/import-handler ./packages/import-handler
|
||||
ADD /packages/readabilityjs ./packages/readabilityjs
|
||||
RUN yarn workspace @omnivore/import-handler build
|
||||
|
||||
# After building, fetch the production dependencies
|
||||
RUN rm -rf /app/packages/import-handler/node_modules
|
||||
RUN rm -rf /app/node_modules
|
||||
RUN yarn install --pure-lockfile --production
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["yarn", "workspace", "@omnivore/import-handler", "start"]
|
||||
@ -3,25 +3,30 @@
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "build/src/index.js",
|
||||
"types": "build/src/index.d.ts",
|
||||
"files": [
|
||||
"build/src"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"keywords": [],
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
|
||||
"lint": "eslint src --ext ts,js,tsx,jsx",
|
||||
"compile": "tsc",
|
||||
"build": "tsc",
|
||||
"start": "functions-framework --source=build/src/ --target=importHandler",
|
||||
"dev": "concurrently \"tsc -w\" \"nodemon --watch ./build/ --exec npm run start\"",
|
||||
"gcloud-deploy": "gcloud functions deploy importHandler --region=$npm_config_region --runtime nodejs14 --trigger-bucket=$npm_config_bucket --env-vars-file=../gcf-shared/env-$npm_config_env.yaml",
|
||||
"deploy": "yarn build && yarn gcloud-deploy"
|
||||
"start": "functions-framework --target=importHandler",
|
||||
"dev": "concurrently \"tsc -w\" \"nodemon --watch ./build/ --exec npm run start\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.11.2",
|
||||
"@types/chai": "^4.3.4",
|
||||
"@types/chai-string": "^1.4.2",
|
||||
"@types/dompurify": "^2.4.0",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/node": "^14.11.2",
|
||||
"@types/unzip-stream": "^0.3.1",
|
||||
"@types/urlsafe-base64": "^1.0.28",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -29,9 +34,18 @@
|
||||
"@google-cloud/functions-framework": "3.1.2",
|
||||
"@google-cloud/storage": "^5.18.1",
|
||||
"@google-cloud/tasks": "^3.0.5",
|
||||
"@omnivore/readability": "1.0.0",
|
||||
"@sentry/serverless": "^7.30.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"axios": "^1.2.2",
|
||||
"csv-parser": "^3.0.0",
|
||||
"dompurify": "^2.4.3",
|
||||
"fs-extra": "^11.1.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"nodemon": "^2.0.15"
|
||||
"linkedom": "^0.14.21",
|
||||
"nodemon": "^2.0.15",
|
||||
"unzip-stream": "^0.3.1",
|
||||
"urlsafe-base64": "^1.0.0",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,24 +5,19 @@
|
||||
|
||||
import { parse } from '@fast-csv/parse'
|
||||
import { Stream } from 'stream'
|
||||
import { ImportContext } from '.'
|
||||
|
||||
export type UrlHandler = (url: URL) => Promise<void>
|
||||
|
||||
export const importCsv = async (
|
||||
stream: Stream,
|
||||
handler: UrlHandler
|
||||
): Promise<number> => {
|
||||
export const importCsv = async (ctx: ImportContext, stream: Stream) => {
|
||||
const parser = parse()
|
||||
stream.pipe(parser)
|
||||
let count = 0
|
||||
for await (const row of parser) {
|
||||
try {
|
||||
const url = new URL(row[0])
|
||||
await handler(url)
|
||||
await ctx.urlHandler(ctx, url)
|
||||
ctx.countImported += 1
|
||||
} catch (error) {
|
||||
console.log('invalid url', row, error)
|
||||
ctx.countFailed += 1
|
||||
}
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
@ -1,41 +1,62 @@
|
||||
import {
|
||||
EventFunction,
|
||||
CloudFunctionsContext,
|
||||
} from '@google-cloud/functions-framework/build/src/functions'
|
||||
import { Storage } from '@google-cloud/storage'
|
||||
import { importCsv, UrlHandler } from './csv'
|
||||
import { importCsv } from './csv'
|
||||
import * as path from 'path'
|
||||
import { importMatterHistory } from './matterHistory'
|
||||
import { importMatterArchive, importMatterHistoryCsv } from './matterHistory'
|
||||
import { Stream } from 'node:stream'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { CONTENT_FETCH_URL, createCloudTask, EMAIL_USER_URL } from './task'
|
||||
import { CONTENT_FETCH_URL, createCloudTask, emailUserUrl } from './task'
|
||||
|
||||
import axios from 'axios'
|
||||
import { promisify } from 'util'
|
||||
import * as jwt from 'jsonwebtoken'
|
||||
import { Readability } from '@omnivore/readability'
|
||||
|
||||
import * as Sentry from '@sentry/serverless'
|
||||
|
||||
Sentry.GCPFunction.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
tracesSampleRate: 0,
|
||||
})
|
||||
|
||||
const signToken = promisify(jwt.sign)
|
||||
|
||||
const storage = new Storage()
|
||||
|
||||
interface StorageEventData {
|
||||
bucket: string
|
||||
const CONTENT_TYPES = ['text/csv', 'application/zip']
|
||||
|
||||
export type UrlHandler = (ctx: ImportContext, url: URL) => Promise<void>
|
||||
export type ContentHandler = (
|
||||
ctx: ImportContext,
|
||||
url: URL,
|
||||
title: string,
|
||||
originalContent: string,
|
||||
parseResult: Readability.ParseResult
|
||||
) => Promise<void>
|
||||
|
||||
export type ImportContext = {
|
||||
userId: string
|
||||
countImported: number
|
||||
countFailed: number
|
||||
urlHandler: UrlHandler
|
||||
contentHandler: ContentHandler
|
||||
}
|
||||
|
||||
type importHandlerFunc = (ctx: ImportContext, stream: Stream) => Promise<void>
|
||||
|
||||
interface StorageEvent {
|
||||
name: string
|
||||
bucket: string
|
||||
contentType: string
|
||||
}
|
||||
|
||||
type importHandlerFunc = (
|
||||
stream: Stream,
|
||||
handler: UrlHandler
|
||||
) => Promise<number>
|
||||
function isStorageEvent(event: any): event is StorageEvent {
|
||||
return 'name' in event && 'bucket' in event && 'contentType' in event
|
||||
}
|
||||
|
||||
const shouldHandle = (data: StorageEventData, ctx: CloudFunctionsContext) => {
|
||||
console.log('deciding to handle', ctx, data)
|
||||
if (ctx.eventType !== 'google.storage.object.finalize') {
|
||||
return false
|
||||
}
|
||||
const shouldHandle = (data: StorageEvent) => {
|
||||
if (
|
||||
!data.name.startsWith('imports/') ||
|
||||
data.contentType.toLowerCase() != 'text/csv'
|
||||
CONTENT_TYPES.indexOf(data.contentType.toLocaleLowerCase()) == -1
|
||||
) {
|
||||
return false
|
||||
}
|
||||
@ -69,7 +90,7 @@ const createEmailCloudTask = async (userId: string, payload: unknown) => {
|
||||
Cookie: `auth=${authToken}`,
|
||||
}
|
||||
|
||||
return createCloudTask(EMAIL_USER_URL, payload, headers)
|
||||
return createCloudTask(emailUserUrl(), payload, headers)
|
||||
}
|
||||
|
||||
const sendImportFailedEmail = async (userId: string) => {
|
||||
@ -86,14 +107,14 @@ const sendImportCompletedEmail = async (
|
||||
) => {
|
||||
return createEmailCloudTask(userId, {
|
||||
subject: 'Your Omnivore import has completed processing',
|
||||
body: `${urlsEnqueued} URLs have been pcoessed and should be available in your library. ${urlsFailed} URLs failed to be parsed.`,
|
||||
body: `${urlsEnqueued} URLs have been processed and should be available in your library. ${urlsFailed} URLs failed to be parsed.`,
|
||||
})
|
||||
}
|
||||
|
||||
const handlerForFile = (name: string): importHandlerFunc | undefined => {
|
||||
const fileName = path.parse(name).name
|
||||
if (fileName.startsWith('MATTER')) {
|
||||
return importMatterHistory
|
||||
return importMatterArchive
|
||||
} else if (fileName.startsWith('URL_LIST')) {
|
||||
return importCsv
|
||||
}
|
||||
@ -101,18 +122,79 @@ const handlerForFile = (name: string): importHandlerFunc | undefined => {
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const importHandler: EventFunction = async (event, context) => {
|
||||
const data = event as StorageEventData
|
||||
const ctx = context as CloudFunctionsContext
|
||||
const urlHandler = async (ctx: ImportContext, url: URL): Promise<void> => {
|
||||
try {
|
||||
// Imports are stored in the format imports/<user id>/<type>-<uuid>.csv
|
||||
const result = await importURL(ctx.userId, url, 'csv-importer')
|
||||
if (result) {
|
||||
ctx.countImported += 1
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('error importing url', err)
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldHandle(data, ctx)) {
|
||||
console.log('handling csv data', data)
|
||||
const sendSavePageMutation = async (userId: string, input: unknown) => {
|
||||
const JWT_SECRET = process.env.JWT_SECRET
|
||||
const REST_BACKEND_ENDPOINT = process.env.REST_BACKEND_ENDPOINT
|
||||
|
||||
const stream = storage
|
||||
.bucket(data.bucket)
|
||||
.file(data.name)
|
||||
.createReadStream()
|
||||
if (!JWT_SECRET || !REST_BACKEND_ENDPOINT) {
|
||||
throw 'Environment not configured correctly'
|
||||
}
|
||||
|
||||
const data = JSON.stringify({
|
||||
query: `mutation SavePage ($input: SavePageInput!){
|
||||
savePage(input:$input){
|
||||
... on SaveSuccess{
|
||||
url
|
||||
clientRequestId
|
||||
}
|
||||
... on SaveError{
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
input: Object.assign({}, input, { source: 'puppeteer-parse' }),
|
||||
},
|
||||
})
|
||||
|
||||
const auth = (await signToken({ uid: userId }, JWT_SECRET)) as string
|
||||
const response = await axios.post(`${REST_BACKEND_ENDPOINT}/graphql`, data, {
|
||||
headers: {
|
||||
Cookie: `auth=${auth};`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
console.log('save page response: ', response)
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
return !!response.data.data.savePage
|
||||
}
|
||||
|
||||
const contentHandler = async (
|
||||
ctx: ImportContext,
|
||||
url: URL,
|
||||
title: string,
|
||||
originalContent: string,
|
||||
parseResult: Readability.ParseResult
|
||||
): Promise<void> => {
|
||||
const requestId = uuid()
|
||||
const apiResponse = await sendSavePageMutation(ctx.userId, {
|
||||
url,
|
||||
clientRequestId: requestId,
|
||||
title,
|
||||
originalContent,
|
||||
parseResult,
|
||||
})
|
||||
if (!apiResponse) {
|
||||
return Promise.reject()
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const handleEvent = async (data: StorageEvent) => {
|
||||
if (shouldHandle(data)) {
|
||||
const handler = handlerForFile(data.name)
|
||||
if (!handler) {
|
||||
console.log('no handler for file:', data.name)
|
||||
@ -131,24 +213,54 @@ export const importHandler: EventFunction = async (event, context) => {
|
||||
return
|
||||
}
|
||||
|
||||
let countFailed = 0
|
||||
let countImported = 0
|
||||
await handler(stream, async (url): Promise<void> => {
|
||||
try {
|
||||
// Imports are stored in the format imports/<user id>/<type>-<uuid>.csv
|
||||
const result = await importURL(userId, url, 'csv-importer')
|
||||
console.log('import url result', result)
|
||||
countImported = countImported + 1
|
||||
} catch (err) {
|
||||
console.log('error importing url', err)
|
||||
countFailed = countFailed + 1
|
||||
}
|
||||
})
|
||||
const stream = storage
|
||||
.bucket(data.bucket)
|
||||
.file(data.name)
|
||||
.createReadStream()
|
||||
|
||||
if (countImported <= 1) {
|
||||
await sendImportFailedEmail(userId)
|
||||
const ctx = {
|
||||
userId,
|
||||
countImported: 0,
|
||||
countFailed: 0,
|
||||
urlHandler,
|
||||
contentHandler,
|
||||
}
|
||||
|
||||
await handler(ctx, stream)
|
||||
|
||||
if (ctx.countImported > 0) {
|
||||
await sendImportCompletedEmail(userId, ctx.countImported, ctx.countFailed)
|
||||
} else {
|
||||
await sendImportCompletedEmail(userId, countImported, countFailed)
|
||||
await sendImportFailedEmail(userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getStorageEvent = (pubSubMessage: string): StorageEvent | undefined => {
|
||||
try {
|
||||
const str = Buffer.from(pubSubMessage, 'base64').toString().trim()
|
||||
const obj = JSON.parse(str) as unknown
|
||||
if (isStorageEvent(obj)) {
|
||||
return obj
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('error deserializing event: ', { pubSubMessage, err })
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const importHandler = Sentry.GCPFunction.wrapHttpFunction(
|
||||
async (req, res) => {
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
if ('message' in req.body && 'data' in req.body.message) {
|
||||
const pubSubMessage = req.body.message.data as string
|
||||
const obj = getStorageEvent(pubSubMessage)
|
||||
if (obj) {
|
||||
await handleEvent(obj)
|
||||
}
|
||||
} else {
|
||||
console.log('no pubsub message')
|
||||
}
|
||||
res.send('ok')
|
||||
}
|
||||
)
|
||||
|
||||
@ -5,28 +5,234 @@
|
||||
|
||||
import { parse } from '@fast-csv/parse'
|
||||
import { Stream } from 'stream'
|
||||
import unzip from 'unzip-stream'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import * as fsExtra from 'fs-extra'
|
||||
|
||||
import { parseHTML } from 'linkedom'
|
||||
import { Readability } from '@omnivore/readability'
|
||||
import createDOMPurify, { SanitizeElementHookEvent } from 'dompurify'
|
||||
|
||||
import { encode } from 'urlsafe-base64'
|
||||
import crypto from 'crypto'
|
||||
import { ImportContext } from '.'
|
||||
|
||||
export type UrlHandler = (url: URL) => Promise<void>
|
||||
|
||||
export const importMatterHistory = async (
|
||||
stream: Stream,
|
||||
handler: UrlHandler
|
||||
): Promise<number> => {
|
||||
export const importMatterHistoryCsv = async (
|
||||
ctx: ImportContext,
|
||||
stream: Stream
|
||||
): Promise<void> => {
|
||||
const parser = parse({
|
||||
headers: true,
|
||||
strictColumnHandling: false,
|
||||
})
|
||||
stream.pipe(parser)
|
||||
|
||||
let count = 0
|
||||
for await (const row of parser) {
|
||||
try {
|
||||
const url = new URL(row['URL'])
|
||||
await handler(url)
|
||||
await ctx.urlHandler(ctx, url)
|
||||
ctx.countImported += 1
|
||||
} catch (error) {
|
||||
console.log('invalid url', row, error)
|
||||
ctx.countFailed += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DOM_PURIFY_CONFIG = {
|
||||
ADD_TAGS: ['iframe'],
|
||||
ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling'],
|
||||
FORBID_ATTR: [
|
||||
'data-ml-dynamic',
|
||||
'data-ml-dynamic-type',
|
||||
'data-orig-url',
|
||||
'data-ml-id',
|
||||
'data-ml',
|
||||
'data-xid',
|
||||
'data-feature',
|
||||
],
|
||||
}
|
||||
|
||||
function domPurifySanitizeHook(node: Element, data: SanitizeElementHookEvent) {
|
||||
if (data.tagName === 'iframe') {
|
||||
const urlRegex = /^(https?:)?\/\/www\.youtube(-nocookie)?\.com\/embed\//i
|
||||
const src = node.getAttribute('src') || ''
|
||||
const dataSrc = node.getAttribute('data-src') || ''
|
||||
|
||||
if (src && urlRegex.test(src)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (dataSrc && urlRegex.test(dataSrc)) {
|
||||
node.setAttribute('src', dataSrc)
|
||||
return
|
||||
}
|
||||
|
||||
node.parentNode?.removeChild(node)
|
||||
}
|
||||
}
|
||||
|
||||
function getPurifiedContent(html: string) {
|
||||
const newWindow = parseHTML('')
|
||||
const DOMPurify = createDOMPurify(newWindow)
|
||||
DOMPurify.addHook('uponSanitizeElement', domPurifySanitizeHook)
|
||||
const clean = DOMPurify.sanitize(html, DOM_PURIFY_CONFIG)
|
||||
return parseHTML(clean).document
|
||||
}
|
||||
|
||||
function createImageProxyUrl(url: string, width = 0, height = 0) {
|
||||
if (process.env.IMAGE_PROXY_URL && process.env.IMAGE_PROXY_SECRET) {
|
||||
const urlWithOptions = `${url}#${width}x${height}`
|
||||
const signature = signImageProxyUrl(urlWithOptions)
|
||||
|
||||
return `${process.env.IMAGE_PROXY_URL}/${width}x${height},s${signature}/${url}`
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
function signImageProxyUrl(url: string) {
|
||||
if (process.env.IMAGE_PROXY_SECRET) {
|
||||
return encode(
|
||||
crypto
|
||||
.createHmac('sha256', process.env.IMAGE_PROXY_SECRET)
|
||||
.update(url)
|
||||
.digest()
|
||||
)
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
async function getReadabilityResult(url: string, originalContent: string) {
|
||||
const document = getPurifiedContent(originalContent)
|
||||
|
||||
try {
|
||||
const article = await new Readability(document, {
|
||||
createImageProxyUrl,
|
||||
url,
|
||||
}).parse()
|
||||
|
||||
if (article) {
|
||||
return article
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('parsing error for url', url, error)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const unarchive = async (stream: Stream): Promise<string> => {
|
||||
const archiveDir = `./archive-${Date.now().toString(16)}`
|
||||
await fsExtra.emptyDir(archiveDir)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
stream
|
||||
.pipe(unzip.Extract({ path: archiveDir }))
|
||||
.on('close', () => {
|
||||
resolve(archiveDir)
|
||||
})
|
||||
.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
const getMatterHistoryContent = (
|
||||
archiveDir: string,
|
||||
row: Record<string, string>
|
||||
) => {
|
||||
try {
|
||||
const contentKey = row['File Id']
|
||||
const contentPath = path.join(archiveDir, contentKey)
|
||||
const content = fs.readFileSync(contentPath).toString()
|
||||
|
||||
return content
|
||||
} catch (err) {
|
||||
console.log('error getting matter history content: ', { row, err })
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const getURL = (str: string | undefined) => {
|
||||
if (!str) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(str)
|
||||
return url
|
||||
} catch (err) {
|
||||
console.log('error parsing url', { str, err })
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const handleMatterHistoryRow = async (
|
||||
ctx: ImportContext,
|
||||
archiveDir: string,
|
||||
row: Record<string, string>
|
||||
) => {
|
||||
const title = row['Title']
|
||||
const urlStr = row['URL']
|
||||
const url = getURL(urlStr)
|
||||
|
||||
if (!url) {
|
||||
ctx.countFailed += 1
|
||||
return
|
||||
}
|
||||
|
||||
const originalContent = getMatterHistoryContent(archiveDir, row)
|
||||
const readabilityResult = originalContent
|
||||
? await getReadabilityResult(urlStr, originalContent)
|
||||
: null
|
||||
|
||||
if (originalContent && readabilityResult) {
|
||||
await ctx.contentHandler(
|
||||
ctx,
|
||||
url,
|
||||
title,
|
||||
originalContent,
|
||||
readabilityResult
|
||||
)
|
||||
} else {
|
||||
await ctx.urlHandler(ctx, url)
|
||||
}
|
||||
}
|
||||
|
||||
export const importMatterArchive = async (
|
||||
ctx: ImportContext,
|
||||
stream: Stream
|
||||
): Promise<void> => {
|
||||
const archiveDir = await unarchive(stream)
|
||||
|
||||
try {
|
||||
const historyFile = path.join(archiveDir, '_matter_history.csv')
|
||||
|
||||
const parser = parse({
|
||||
headers: true,
|
||||
strictColumnHandling: false,
|
||||
})
|
||||
|
||||
fs.createReadStream(historyFile).pipe(parser)
|
||||
|
||||
for await (const row of parser) {
|
||||
try {
|
||||
await handleMatterHistoryRow(ctx, archiveDir, row)
|
||||
ctx.countImported += 1
|
||||
} catch (error) {
|
||||
console.log('invalid url', row, error)
|
||||
ctx.countFailed += 1
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('error handling archive: ', { err })
|
||||
} finally {
|
||||
try {
|
||||
await fsExtra.rm(archiveDir, { recursive: true, force: true })
|
||||
} catch (err) {
|
||||
console.log('Error removing archive directory', { err })
|
||||
}
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
108
packages/import-handler/src/readability.d.ts
vendored
Normal file
108
packages/import-handler/src/readability.d.ts
vendored
Normal file
@ -0,0 +1,108 @@
|
||||
// Type definitions for non-npm package mozilla-readability 0.2
|
||||
// Project: https://github.com/mozilla/readability
|
||||
// Definitions by: Charles Vandevoorde <https://github.com/charlesvdv>, Alex Wendland <https://github.com/awendland>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
// TypeScript Version: 2.2
|
||||
|
||||
declare module '@omnivore/readability' {
|
||||
class Readability {
|
||||
constructor(doc: Document, options?: Readability.Options)
|
||||
|
||||
async parse(): Promise<Readability.ParseResult | null>
|
||||
}
|
||||
|
||||
namespace Readability {
|
||||
interface Options {
|
||||
/**
|
||||
* Control whether log messages are sent to the console
|
||||
*/
|
||||
debug?: boolean
|
||||
|
||||
/**
|
||||
* Set a maximum size on the documents that will be processed. This size is
|
||||
* checked before any parsing operations occur. If the number of elements in
|
||||
* the document exceeds this threshold then an Error will be thrown.
|
||||
*
|
||||
* See implementation details at https://github.com/mozilla/readability/blob/52ab9b5c8916c306a47b2119270dcdabebf9d203/Readability.js#L2019
|
||||
*/
|
||||
maxElemsToParse?: number
|
||||
|
||||
nbTopCandidates?: number
|
||||
|
||||
/**
|
||||
* Minimum number of characters in the extracted textContent in order to
|
||||
* consider the article correctly identified. If the threshold is not met then
|
||||
* the extraction process will automatically run again with different flags.
|
||||
*
|
||||
* See implementation details at https://github.com/mozilla/readability/blob/52ab9b5c8916c306a47b2119270dcdabebf9d203/Readability.js#L1208
|
||||
*
|
||||
* Changed from wordThreshold in https://github.com/mozilla/readability/commit/3ff9a166fb27928f222c4c0722e730eda412658a
|
||||
*/
|
||||
charThreshold?: number
|
||||
|
||||
/**
|
||||
* parse() removes the class="" attribute from every element in the given
|
||||
* subtree, except those that match CLASSES_TO_PRESERVE and
|
||||
* the classesToPreserve array from the options object.
|
||||
*/
|
||||
classesToPreserve?: string[]
|
||||
|
||||
/**
|
||||
* By default Readability will strip all classes from the HTML elements in the
|
||||
* processed article. By setting this to `true` the classes will be retained.
|
||||
*
|
||||
* This is a blanket alternative to `classesToPreserve`.
|
||||
*
|
||||
* Added in https://github.com/mozilla/readability/commit/2982216913af2c66b0690e88606b03116553ad92
|
||||
*/
|
||||
|
||||
keepClasses?: boolean
|
||||
url?: string
|
||||
|
||||
/**
|
||||
* Function that converts a regular image url into imageproxy url
|
||||
* @param url string
|
||||
*/
|
||||
createImageProxyUrl?: (
|
||||
url: string,
|
||||
width?: number,
|
||||
height?: number
|
||||
) => string
|
||||
|
||||
/**
|
||||
* By default, Readability will clean all tables from the HTML elements in the
|
||||
* processed article. But newsletters in emails use tables to display their content.
|
||||
* By setting this to `true`, these tables will be retained.
|
||||
*/
|
||||
keepTables?: boolean
|
||||
}
|
||||
|
||||
interface ParseResult {
|
||||
/** Article title */
|
||||
title: string
|
||||
/** Author metadata */
|
||||
byline?: string | null
|
||||
/** Content direction */
|
||||
dir?: string | null
|
||||
/** HTML string of processed article content */
|
||||
content: string
|
||||
/** non-HTML version of `content` */
|
||||
textContent: string
|
||||
/** Length of an article, in characters */
|
||||
length: number
|
||||
/** Article description, or short excerpt from the content */
|
||||
excerpt: string
|
||||
/** Article site name */
|
||||
siteName?: string | null
|
||||
/** Article site icon */
|
||||
siteIcon?: string | null
|
||||
/** Article preview image */
|
||||
previewImage?: string | null
|
||||
/** Article published date */
|
||||
publishedDate?: Date | null
|
||||
language?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export { Readability }
|
||||
}
|
||||
@ -3,12 +3,13 @@ import { CloudTasksClient, protos } from '@google-cloud/tasks'
|
||||
|
||||
const cloudTask = new CloudTasksClient()
|
||||
|
||||
export const EMAIL_USER_URL = (() => {
|
||||
if (!process.env.INTERNAL_SVC_ENDPOINT) {
|
||||
throw `Environment not configured correctly, no SVC endpoint`
|
||||
export const emailUserUrl = () => {
|
||||
const envar = process.env.INTERNAL_SVC_ENDPOINT
|
||||
if (envar) {
|
||||
return envar + 'api/user/email'
|
||||
}
|
||||
return (process.env.INTERNAL_SVC_ENDPOINT ?? '') + 'api/user/email'
|
||||
})()
|
||||
throw 'INTERNAL_SVC_ENDPOINT not set'
|
||||
}
|
||||
|
||||
export const CONTENT_FETCH_URL = process.env.CONTENT_FETCH_GCF_URL
|
||||
|
||||
|
||||
@ -4,6 +4,8 @@ import { expect } from 'chai'
|
||||
import chaiString from 'chai-string'
|
||||
import * as fs from 'fs'
|
||||
import { importCsv } from '../../src/csv'
|
||||
import { ImportContext } from '../../src'
|
||||
import { stubImportCtx } from '../util'
|
||||
|
||||
chai.use(chaiString)
|
||||
|
||||
@ -11,11 +13,15 @@ describe('Load a simple CSV file', () => {
|
||||
it('should call the handler for each URL', async () => {
|
||||
const urls: URL[] = []
|
||||
const stream = fs.createReadStream('./test/csv/data/simple.csv')
|
||||
const count = await importCsv(stream, (url): Promise<void> => {
|
||||
const stub = stubImportCtx()
|
||||
stub.urlHandler = (ctx: ImportContext, url): Promise<void> => {
|
||||
urls.push(url)
|
||||
return Promise.resolve()
|
||||
})
|
||||
expect(count).to.equal(2)
|
||||
}
|
||||
|
||||
await importCsv(stub, stream)
|
||||
expect(stub.countFailed).to.equal(0)
|
||||
expect(stub.countImported).to.equal(2)
|
||||
expect(urls).to.eql([
|
||||
new URL('https://omnivore.app'),
|
||||
new URL('https://google.com'),
|
||||
|
||||
BIN
packages/import-handler/test/matter/data/Archive.zip
Normal file
BIN
packages/import-handler/test/matter/data/Archive.zip
Normal file
Binary file not shown.
@ -3,7 +3,13 @@ import * as chai from 'chai'
|
||||
import { expect } from 'chai'
|
||||
import chaiString from 'chai-string'
|
||||
import * as fs from 'fs'
|
||||
import { importMatterHistory } from '../../src/matterHistory'
|
||||
import {
|
||||
importMatterArchive,
|
||||
importMatterHistoryCsv,
|
||||
} from '../../src/matterHistory'
|
||||
import { stubImportCtx } from '../util'
|
||||
import { ImportContext } from '../../src'
|
||||
import { Readability } from '@omnivore/readability'
|
||||
|
||||
chai.use(chaiString)
|
||||
|
||||
@ -11,11 +17,40 @@ describe('Load a simple _matter_history file', () => {
|
||||
it('should find the URL of each row', async () => {
|
||||
const urls: URL[] = []
|
||||
const stream = fs.createReadStream('./test/matter/data/_matter_history.csv')
|
||||
const count = await importMatterHistory(stream, (url): Promise<void> => {
|
||||
const stub = stubImportCtx()
|
||||
stub.urlHandler = (ctx: ImportContext, url): Promise<void> => {
|
||||
urls.push(url)
|
||||
return Promise.resolve()
|
||||
})
|
||||
expect(count).to.equal(1)
|
||||
}
|
||||
|
||||
await importMatterHistoryCsv(stub, stream)
|
||||
expect(stub.countFailed).to.equal(0)
|
||||
expect(stub.countImported).to.equal(1)
|
||||
expect(urls).to.eql([
|
||||
new URL('https://www.bloomberg.com/features/2022-the-crypto-story/'),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Load archive file', () => {
|
||||
it('should find the URL of each row', async () => {
|
||||
const urls: URL[] = []
|
||||
const stream = fs.createReadStream('./test/matter/data/Archive.zip')
|
||||
const stub = stubImportCtx()
|
||||
stub.contentHandler = (
|
||||
ctx: ImportContext,
|
||||
url: URL,
|
||||
title: string,
|
||||
originalContent: string,
|
||||
parseResult: Readability.ParseResult
|
||||
): Promise<void> => {
|
||||
urls.push(url)
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
await importMatterArchive(stub, stream)
|
||||
expect(stub.countFailed).to.equal(0)
|
||||
expect(stub.countImported).to.equal(1)
|
||||
expect(urls).to.eql([
|
||||
new URL('https://www.bloomberg.com/features/2022-the-crypto-story/'),
|
||||
])
|
||||
|
||||
22
packages/import-handler/test/util.ts
Normal file
22
packages/import-handler/test/util.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Readability } from '@omnivore/readability'
|
||||
import { ImportContext } from '../src'
|
||||
|
||||
export const stubImportCtx = () => {
|
||||
return {
|
||||
userId: '',
|
||||
countImported: 0,
|
||||
countFailed: 0,
|
||||
urlHandler: (ctx: ImportContext, url: URL): Promise<void> => {
|
||||
return Promise.resolve()
|
||||
},
|
||||
contentHandler: (
|
||||
ctx: ImportContext,
|
||||
url: URL,
|
||||
title: string,
|
||||
originalContent: string,
|
||||
parseResult: Readability.ParseResult
|
||||
): Promise<void> => {
|
||||
return Promise.resolve()
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,12 @@
|
||||
{
|
||||
"extends": "@tsconfig/node14/tsconfig.json",
|
||||
"extends": "./../../tsconfig.json",
|
||||
"ts-node": { "files": true },
|
||||
"compilerOptions": {
|
||||
"outDir": "build",
|
||||
"rootDir": ".",
|
||||
"lib": ["dom"]
|
||||
"lib": ["dom"],
|
||||
// Generate d.ts files
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
|
||||
@ -15,9 +15,8 @@ export function ConfirmProfileModal(): JSX.Element {
|
||||
const [username, setUsername] = useState('')
|
||||
const [debouncedUsername, setDebouncedUsername] = useState('')
|
||||
const [bio, setBio] = useState('')
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [errorMessage, setErrorMessage] =
|
||||
useState<string | undefined>(undefined)
|
||||
|
||||
const { isUsernameValid, usernameErrorMessage } = useValidateUsernameQuery({
|
||||
username: debouncedUsername,
|
||||
@ -110,8 +109,9 @@ export function ConfirmProfileModal(): JSX.Element {
|
||||
<HStack
|
||||
css={{
|
||||
width: '100%',
|
||||
borderRadius: '6px',
|
||||
border: `1px solid $grayBorder`,
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #3D3D3D',
|
||||
boxShadow: '#B1B1B1 9px 9px 9px -9px',
|
||||
p: '$2',
|
||||
}}
|
||||
>
|
||||
@ -139,7 +139,12 @@ export function ConfirmProfileModal(): JSX.Element {
|
||||
{isUsernameValid && (
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{ m: 0, pl: '$2', alignSelf: 'flex-start', color: '$omnivoreGray' }}
|
||||
css={{
|
||||
m: 0,
|
||||
pl: '$2',
|
||||
alignSelf: 'flex-start',
|
||||
color: '$omnivoreGray',
|
||||
}}
|
||||
>
|
||||
Username is available.
|
||||
</StyledText>
|
||||
|
||||
@ -12,7 +12,12 @@ export function ProfileLayout(props: ProfileLayoutProps): JSX.Element {
|
||||
<VStack
|
||||
alignment="center"
|
||||
distribution="center"
|
||||
css={{ bg: '$omnivoreYellow', height: '100vh' }}
|
||||
css={{
|
||||
// bg: '$omnivoreYellow',
|
||||
height: '100vh',
|
||||
background:
|
||||
'-webkit-linear-gradient(-65deg, rgba(255, 255, 255, 1.0) 45%, rgba(255, 210, 52, 1.0) 0%)',
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</VStack>
|
||||
|
||||
@ -12,9 +12,8 @@ import { parseErrorCodes } from '../../../lib/queryParamParser'
|
||||
export function EmailForgotPassword(): JSX.Element {
|
||||
const router = useRouter()
|
||||
const [email, setEmail] = useState<string>('')
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [errorMessage, setErrorMessage] =
|
||||
useState<string | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return
|
||||
@ -36,7 +35,8 @@ export function EmailForgotPassword(): JSX.Element {
|
||||
width: '70vw',
|
||||
maxWidth: '576px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: 'rgb(224 224 224) 9px 9px 9px -9px',
|
||||
border: '1px solid #3D3D3D',
|
||||
boxShadow: '#B1B1B1 9px 9px 9px -9px',
|
||||
}}
|
||||
>
|
||||
<StyledText style="subHeadline" css={{ color: '$omnivoreGray' }}>
|
||||
|
||||
@ -15,9 +15,8 @@ export function EmailLogin(): JSX.Element {
|
||||
const router = useRouter()
|
||||
const [email, setEmail] = useState<string | undefined>(undefined)
|
||||
const [password, setPassword] = useState<string | undefined>(undefined)
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [errorMessage, setErrorMessage] =
|
||||
useState<string | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return
|
||||
@ -39,7 +38,8 @@ export function EmailLogin(): JSX.Element {
|
||||
width: '70vw',
|
||||
maxWidth: '576px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: 'rgb(224 224 224) 9px 9px 9px -9px',
|
||||
border: '1px solid #3D3D3D',
|
||||
boxShadow: '#B1B1B1 9px 9px 9px -9px',
|
||||
}}
|
||||
>
|
||||
<StyledText style="subHeadline" css={{ color: '$omnivoreGray' }}>
|
||||
|
||||
@ -13,9 +13,8 @@ export function EmailResetPassword(): JSX.Element {
|
||||
const router = useRouter()
|
||||
const [token, setToken] = useState<string | undefined>(undefined)
|
||||
const [password, setPassword] = useState<string>('')
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [errorMessage, setErrorMessage] =
|
||||
useState<string | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return
|
||||
@ -45,7 +44,8 @@ export function EmailResetPassword(): JSX.Element {
|
||||
width: '70vw',
|
||||
maxWidth: '576px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: 'rgb(224 224 224) 9px 9px 9px -9px',
|
||||
border: '1px solid #3D3D3D',
|
||||
boxShadow: '#B1B1B1 9px 9px 9px -9px',
|
||||
}}
|
||||
>
|
||||
<VStack
|
||||
|
||||
@ -19,12 +19,10 @@ export function EmailSignup(): JSX.Element {
|
||||
const [password, setPassword] = useState<string | undefined>(undefined)
|
||||
const [fullname, setFullname] = useState<string | undefined>(undefined)
|
||||
const [username, setUsername] = useState<string | undefined>(undefined)
|
||||
const [debouncedUsername, setDebouncedUsername] = useState<
|
||||
string | undefined
|
||||
>(undefined)
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [debouncedUsername, setDebouncedUsername] =
|
||||
useState<string | undefined>(undefined)
|
||||
const [errorMessage, setErrorMessage] =
|
||||
useState<string | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return
|
||||
@ -60,7 +58,8 @@ export function EmailSignup(): JSX.Element {
|
||||
width: '70vw',
|
||||
maxWidth: '576px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: 'rgb(224 224 224) 9px 9px 9px -9px',
|
||||
border: '1px solid #3D3D3D',
|
||||
boxShadow: '#B1B1B1 9px 9px 9px -9px',
|
||||
}}
|
||||
>
|
||||
<StyledText style="subHeadline" css={{ color: '$omnivoreGray' }}>
|
||||
|
||||
@ -17,9 +17,8 @@ export default function InvitePage(): JSX.Element {
|
||||
const router = useRouter()
|
||||
const { viewerData, viewerDataError, isLoading } = useGetViewerQuery()
|
||||
const { inviteCode } = router.query
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [errorMessage, setErrorMessage] =
|
||||
useState<string | undefined>(undefined)
|
||||
|
||||
// Check if the user is logged in and display an error message if they are not
|
||||
useEffect(() => {
|
||||
@ -77,7 +76,8 @@ export default function InvitePage(): JSX.Element {
|
||||
width: '70vw',
|
||||
maxWidth: '576px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: 'rgb(224 224 224) 9px 9px 9px -9px',
|
||||
border: '1px solid #3D3D3D',
|
||||
boxShadow: '#B1B1B1 9px 9px 9px -9px',
|
||||
}}
|
||||
>
|
||||
<StyledText style="subHeadline" css={{ color: '$omnivoreGray' }}>
|
||||
|
||||
@ -96,7 +96,8 @@ export default function ImportUploader(): JSX.Element {
|
||||
width: '70vw',
|
||||
maxWidth: '576px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: 'rgb(224 224 224) 9px 9px 9px -9px',
|
||||
border: '1px solid #3D3D3D',
|
||||
boxShadow: '#B1B1B1 9px 9px 9px -9px',
|
||||
}}
|
||||
>
|
||||
<StyledText
|
||||
|
||||
218
packages/web/pages/tools/import/matter-archive.tsx
Normal file
218
packages/web/pages/tools/import/matter-archive.tsx
Normal file
@ -0,0 +1,218 @@
|
||||
import { ChangeEvent, useCallback, useMemo, useState } from 'react'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { applyStoredTheme } from '../../../lib/themeUpdater'
|
||||
|
||||
import {
|
||||
Box,
|
||||
HStack,
|
||||
VStack,
|
||||
} from '../../../components/elements/LayoutPrimitives'
|
||||
|
||||
import 'antd/dist/antd.compact.css'
|
||||
import { StyledText } from '../../../components/elements/StyledText'
|
||||
import { ProfileLayout } from '../../../components/templates/ProfileLayout'
|
||||
import {
|
||||
uploadImportFileRequestMutation,
|
||||
UploadImportFileType,
|
||||
} from '../../../lib/networking/mutations/uploadImportFileMutation'
|
||||
import { Button } from '../../../components/elements/Button'
|
||||
import Dropzone, { useDropzone } from 'react-dropzone'
|
||||
|
||||
import { SyncLoader } from 'react-spinners'
|
||||
import { theme } from '../../../components/tokens/stitches.config'
|
||||
import { Tray } from 'phosphor-react'
|
||||
|
||||
type UploadState = 'none' | 'uploading' | 'completed'
|
||||
|
||||
export default function ImportUploader(): JSX.Element {
|
||||
applyStoredTheme(false)
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>()
|
||||
const [file, setFile] = useState<File>()
|
||||
const [type, setType] = useState<UploadImportFileType>()
|
||||
const [uploadState, setUploadState] = useState<UploadState>('none')
|
||||
const { acceptedFiles, getRootProps, getInputProps } = useDropzone()
|
||||
|
||||
const onDropAccepted = async (acceptedFiles: File[]) => {
|
||||
const contentType = 'application/zip'
|
||||
const file = acceptedFiles.find(() => true)
|
||||
if (!file) {
|
||||
setErrorMessage('No file selected.')
|
||||
return
|
||||
}
|
||||
|
||||
setUploadState('uploading')
|
||||
|
||||
try {
|
||||
const result = await uploadImportFileRequestMutation(
|
||||
UploadImportFileType.MATTER,
|
||||
contentType
|
||||
)
|
||||
|
||||
if (result && result.uploadSignedUrl) {
|
||||
const uploadRes = await fetch(result.uploadSignedUrl, {
|
||||
method: 'PUT',
|
||||
body: file,
|
||||
headers: {
|
||||
'content-type': contentType,
|
||||
'content-length': `${file.size}`,
|
||||
},
|
||||
})
|
||||
setUploadState('completed')
|
||||
} else {
|
||||
setErrorMessage(
|
||||
'Unable to create file upload. Please ensure you are logged in.'
|
||||
)
|
||||
setUploadState('none')
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('caught error', error)
|
||||
if (error == 'UPLOAD_DAILY_LIMIT_EXCEEDED') {
|
||||
setErrorMessage('You have exceeded your maximum daily upload limit.')
|
||||
}
|
||||
setUploadState('none')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ProfileLayout>
|
||||
<VStack
|
||||
alignment="start"
|
||||
css={{
|
||||
padding: '16px',
|
||||
background: 'white',
|
||||
minWidth: '340px',
|
||||
width: '70vw',
|
||||
maxWidth: '576px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #3D3D3D',
|
||||
boxShadow: '#B1B1B1 9px 9px 9px -9px',
|
||||
}}
|
||||
>
|
||||
<StyledText
|
||||
style="modalHeadline"
|
||||
css={{
|
||||
color: theme.colors.omnivoreGray.toString(),
|
||||
}}
|
||||
>
|
||||
Import Matter Archive
|
||||
</StyledText>
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{ pt: '10px', color: theme.colors.omnivoreGray.toString() }}
|
||||
>
|
||||
Omnivore supports uploading the Archive.zip file generated by
|
||||
exporting your data from the Matter app.
|
||||
</StyledText>
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{ color: theme.colors.omnivoreGray.toString() }}
|
||||
>
|
||||
To export your data from Matter, go to My Account, and choose Export
|
||||
data, this will send you an email with your data in a file
|
||||
Archive.zip. Upload that file using the uploader on this page.
|
||||
</StyledText>
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{ color: theme.colors.omnivoreGray.toString() }}
|
||||
>
|
||||
<a href="https://docs.omnivore.app/">More info</a>
|
||||
</StyledText>
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{ pt: '20px', color: theme.colors.omnivoreGray.toString() }}
|
||||
>
|
||||
<b>Note:</b> Please note you are limited to three import uploads per a
|
||||
day, and the maximum file size is 10MB.
|
||||
</StyledText>
|
||||
<VStack css={{ pt: '36px', width: '100%' }}>
|
||||
{uploadState == 'completed' ? (
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{
|
||||
pt: '10px',
|
||||
pb: '20px',
|
||||
color: theme.colors.omnivoreGray.toString(),
|
||||
}}
|
||||
>
|
||||
Your upload has completed. Please note that it can take some time
|
||||
for your library to be updated. You will be sent an email when the
|
||||
process completes.
|
||||
</StyledText>
|
||||
) : (
|
||||
<>
|
||||
<Box css={{ width: '100%' }}>
|
||||
<Dropzone
|
||||
accept={{ 'application/zip': ['.zip'] }}
|
||||
multiple={false}
|
||||
maxSize={1e7}
|
||||
onDropAccepted={onDropAccepted}
|
||||
disabled={uploadState == 'uploading'}
|
||||
>
|
||||
{({ getRootProps, getInputProps }) => (
|
||||
<div {...getRootProps()}>
|
||||
<input {...getInputProps()} />
|
||||
<VStack
|
||||
alignment="center"
|
||||
css={{
|
||||
borderRadius: '8px',
|
||||
border: '1px dashed #d9d9d9',
|
||||
minHeight: '64px',
|
||||
mb: '10px',
|
||||
pt: '10px',
|
||||
}}
|
||||
>
|
||||
{uploadState == 'uploading' ? (
|
||||
<SyncLoader
|
||||
color={theme.colors.omnivoreGray.toString()}
|
||||
size={8}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Tray
|
||||
size={34}
|
||||
color={theme.colors.omnivoreGray.toString()}
|
||||
/>
|
||||
<StyledText
|
||||
style="caption"
|
||||
css={{
|
||||
pt: '10px',
|
||||
color: theme.colors.omnivoreGray.toString(),
|
||||
}}
|
||||
>
|
||||
Click or Drag Archive.zip file here to upload
|
||||
</StyledText>
|
||||
</>
|
||||
)}
|
||||
</VStack>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{uploadState == 'completed' && (
|
||||
<VStack css={{ width: '100%' }} alignment="center">
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
window.location.href = '/home'
|
||||
e.preventDefault()
|
||||
}}
|
||||
style="ctaDarkYellow"
|
||||
>
|
||||
Return to Library
|
||||
</Button>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<StyledText style="error">{errorMessage}</StyledText>
|
||||
)}
|
||||
</VStack>
|
||||
</VStack>
|
||||
</ProfileLayout>
|
||||
)
|
||||
}
|
||||
173
yarn.lock
173
yarn.lock
@ -5526,6 +5526,15 @@
|
||||
"@sentry/utils" "6.19.6"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/core@7.30.0":
|
||||
version "7.30.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.30.0.tgz#02d2e8747484ba64b6d712e8afe6736250efdc26"
|
||||
integrity sha512-NeLigkBlpcK63ymM63GoIHurml6V3BUe1Vi+trwm4/qqOTzT7PQhvdJCX+o3+atzRBH+zdb6kd4VWx44Oye3KA==
|
||||
dependencies:
|
||||
"@sentry/types" "7.30.0"
|
||||
"@sentry/utils" "7.30.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/hub@5.30.0":
|
||||
version "5.30.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.30.0.tgz#2453be9b9cb903404366e198bd30c7ca74cdc100"
|
||||
@ -5652,6 +5661,19 @@
|
||||
lru_map "^0.3.3"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/node@7.30.0":
|
||||
version "7.30.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.30.0.tgz#42ef5b29d065f6de6ac5556a56aca20d3b9073e1"
|
||||
integrity sha512-YYasu6C3I0HBP4N1oc/ed2nunxhGJgtAWaKwq3lo8uk3uF6cB1A8+2e0CpjzU5ejhbaFPUBxHyj4th39Bvku/w==
|
||||
dependencies:
|
||||
"@sentry/core" "7.30.0"
|
||||
"@sentry/types" "7.30.0"
|
||||
"@sentry/utils" "7.30.0"
|
||||
cookie "^0.4.1"
|
||||
https-proxy-agent "^5.0.0"
|
||||
lru_map "^0.3.3"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/node@^5.26.0":
|
||||
version "5.30.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.30.0.tgz#4ca479e799b1021285d7fe12ac0858951c11cd48"
|
||||
@ -5693,6 +5715,19 @@
|
||||
"@types/express" "^4.17.2"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/serverless@^7.30.0":
|
||||
version "7.30.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/serverless/-/serverless-7.30.0.tgz#f639c835043de2c3dec10f3667fcf8545a557e24"
|
||||
integrity sha512-Fs6fuyDR+RJ5yg/m4M9Ids+jVc9FJbE8XBxOGZkpYkjZFLWVETMI3tJt4cF31LOQF/irftr6pKW/blxHAknBrg==
|
||||
dependencies:
|
||||
"@sentry/node" "7.30.0"
|
||||
"@sentry/tracing" "7.30.0"
|
||||
"@sentry/types" "7.30.0"
|
||||
"@sentry/utils" "7.30.0"
|
||||
"@types/aws-lambda" "^8.10.62"
|
||||
"@types/express" "^4.17.14"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/tracing@5.30.0":
|
||||
version "5.30.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.30.0.tgz#501d21f00c3f3be7f7635d8710da70d9419d4e1f"
|
||||
@ -5726,6 +5761,16 @@
|
||||
"@sentry/utils" "6.19.6"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/tracing@7.30.0":
|
||||
version "7.30.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.30.0.tgz#b4576fbaf81ce418f1b7c37e7e5f4f6cf19a3c3b"
|
||||
integrity sha512-bjGeDeKhpGAmLcWcrXFT/xOfHVwp/j0L1aRHzYHnqgTjVzD0NXcooPu/Nz8vF0paxz+hPD5bJwb8kz/ggJzGWQ==
|
||||
dependencies:
|
||||
"@sentry/core" "7.30.0"
|
||||
"@sentry/types" "7.30.0"
|
||||
"@sentry/utils" "7.30.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/tracing@^7.9.0":
|
||||
version "7.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.9.0.tgz#0cbbf5b61ee76b934d2e4160a0ad3daf0001237b"
|
||||
@ -5756,6 +5801,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.10.0.tgz#c91d634768336238ac30ed750fa918326c384cbb"
|
||||
integrity sha512-1UBwdbS0xXzANzp63g4eNQly/qKIXp0swP5OTKWoADvKBtL4anroLUA/l8ADMtuwFZYtVANc8WRGxM2+YmaXtg==
|
||||
|
||||
"@sentry/types@7.30.0":
|
||||
version "7.30.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.30.0.tgz#fc2baeb5b0e1ecc4d52b07b056fcba54449cd9ce"
|
||||
integrity sha512-l4A86typvt/SfWh5JffpdxNGkg5EEA8m35BzpIcKmCAQZUDmnb4b478r8jdD2uuOjLmPNmZr1tifdRW4NCLuxQ==
|
||||
|
||||
"@sentry/types@7.9.0":
|
||||
version "7.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.9.0.tgz#8fa952865fda76f7c7c7fc6c84043979a22043ae"
|
||||
@ -5793,6 +5843,14 @@
|
||||
"@sentry/types" "7.10.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/utils@7.30.0":
|
||||
version "7.30.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.30.0.tgz#1d83145399c65e31f725c1b6ae02f451a990f326"
|
||||
integrity sha512-tSlBhr5u/LdE2emxIDTDmjmyRr99GnZGIAh5GwRxUgeDQ3VEfNUFlyFodBCbZ6yeYTYd6PWNih5xoHn1+Rf3Sw==
|
||||
dependencies:
|
||||
"@sentry/types" "7.30.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/utils@7.9.0":
|
||||
version "7.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.9.0.tgz#497c41efe1b32974208ca68570e42c853b874f77"
|
||||
@ -7865,6 +7923,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.21.tgz#9f35a5643129df132cf3b5c1ec64046ea1af0650"
|
||||
integrity sha512-yd+9qKmJxm496BOV9CMNaey8TWsikaZOwMRwPHQIjcOJM9oV+fi9ZMNw3JsVnbEEbo2gRTDnGEBv8pjyn67hNg==
|
||||
|
||||
"@types/chai@^4.3.4":
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.4.tgz#e913e8175db8307d78b4e8fa690408ba6b65dee4"
|
||||
integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==
|
||||
|
||||
"@types/chrome@^0.0.197":
|
||||
version "0.0.197"
|
||||
resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.197.tgz#c1b50cdb72ee40f9bc1411506031a9f8a925ab35"
|
||||
@ -7948,6 +8011,13 @@
|
||||
dependencies:
|
||||
"@types/trusted-types" "*"
|
||||
|
||||
"@types/dompurify@^2.4.0":
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.4.0.tgz#fd9706392a88e0e0e6d367f3588482d817df0ab9"
|
||||
integrity sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg==
|
||||
dependencies:
|
||||
"@types/trusted-types" "*"
|
||||
|
||||
"@types/duplexify@^3.6.0":
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/duplexify/-/duplexify-3.6.0.tgz#dfc82b64bd3a2168f5bd26444af165bf0237dcd8"
|
||||
@ -8009,7 +8079,7 @@
|
||||
dependencies:
|
||||
"@types/express" "*"
|
||||
|
||||
"@types/express@*", "@types/express@^4.16.0", "@types/express@^4.17.13", "@types/express@^4.17.2", "@types/express@^4.17.7":
|
||||
"@types/express@*", "@types/express@^4.16.0", "@types/express@^4.17.13", "@types/express@^4.17.14", "@types/express@^4.17.2", "@types/express@^4.17.7":
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.15.tgz#9290e983ec8b054b65a5abccb610411953d417ff"
|
||||
integrity sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ==
|
||||
@ -8058,6 +8128,14 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/fs-extra@^11.0.1":
|
||||
version "11.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-11.0.1.tgz#f542ec47810532a8a252127e6e105f487e0a6ea5"
|
||||
integrity sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==
|
||||
dependencies:
|
||||
"@types/jsonfile" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/glob@*":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
|
||||
@ -8225,6 +8303,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
|
||||
|
||||
"@types/jsonfile@*":
|
||||
version "6.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/jsonfile/-/jsonfile-6.1.1.tgz#ac84e9aefa74a2425a0fb3012bdea44f58970f1b"
|
||||
integrity sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/jsonwebtoken@^8.5.0":
|
||||
version "8.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz#2531d5e300803aa63279b232c014acf780c981c5"
|
||||
@ -8313,6 +8398,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
|
||||
integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
|
||||
|
||||
"@types/mocha@^10.0.1":
|
||||
version "10.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.1.tgz#2f4f65bb08bc368ac39c96da7b2f09140b26851b"
|
||||
integrity sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==
|
||||
|
||||
"@types/mocha@^8.2.2":
|
||||
version "8.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.3.tgz#bbeb55fbc73f28ea6de601fbfa4613f58d785323"
|
||||
@ -8653,10 +8743,17 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
|
||||
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
|
||||
|
||||
"@types/unzip-stream@^0.3.1":
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/unzip-stream/-/unzip-stream-0.3.1.tgz#a57b8365be3d7f89960c755c8cc37c83223dc7d0"
|
||||
integrity sha512-RlE3qaqvu4XaMwxkG/zR1gIunCbqXvNrmZ4BCG7OiQ8QUactFUPxm0TTrOCRJZQfPW3T6XBH7PcHQiiqkdcijw==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/urlsafe-base64@^1.0.28":
|
||||
version "1.0.28"
|
||||
resolved "https://registry.yarnpkg.com/@types/urlsafe-base64/-/urlsafe-base64-1.0.28.tgz#2cf2098518e98c730a7e00de79a455e0269a3f4d"
|
||||
integrity sha1-LPIJhRjpjHMKfgDeeaRV4CaaP00=
|
||||
integrity sha512-TG5dKbqx75FUTXfiARIPvLvMCImVYJbKM+Fvy9HgpxkunHnMHNAn78xpvcZxIbPITyRzf0b2Gl8fnd1Ja3p1eQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
@ -8665,6 +8762,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
|
||||
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
|
||||
|
||||
"@types/uuid@^9.0.0":
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.0.tgz#53ef263e5239728b56096b0a869595135b7952d2"
|
||||
integrity sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==
|
||||
|
||||
"@types/voca@^1.4.0":
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/voca/-/voca-1.4.2.tgz#4d2ceef3582c2a1f6a1574f71f0897400d910583"
|
||||
@ -10164,6 +10266,15 @@ axios@^1.2.0:
|
||||
form-data "^4.0.0"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
axios@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.2.tgz#72681724c6e6a43a9fea860fc558127dbe32f9f1"
|
||||
integrity sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.0"
|
||||
form-data "^4.0.0"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
axobject-query@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
|
||||
@ -10494,6 +10605,14 @@ binary-extensions@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9"
|
||||
integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==
|
||||
|
||||
binary@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79"
|
||||
integrity sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==
|
||||
dependencies:
|
||||
buffers "~0.1.1"
|
||||
chainsaw "~0.1.0"
|
||||
|
||||
binaryextensions@^2.1.2:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.3.0.tgz#1d269cbf7e6243ea886aa41453c3651ccbe13c22"
|
||||
@ -10793,6 +10912,11 @@ buffer@^6.0.3:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.2.1"
|
||||
|
||||
buffers@~0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb"
|
||||
integrity sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==
|
||||
|
||||
bufrw@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/bufrw/-/bufrw-1.3.0.tgz#28d6cfdaf34300376836310f5c31d57eeb40c8fa"
|
||||
@ -11089,6 +11213,13 @@ chai@^4.3.6:
|
||||
pathval "^1.1.1"
|
||||
type-detect "^4.0.5"
|
||||
|
||||
chainsaw@~0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98"
|
||||
integrity sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==
|
||||
dependencies:
|
||||
traverse ">=0.3.0 <0.4"
|
||||
|
||||
chalk@^1.0.0, chalk@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
|
||||
@ -13062,6 +13193,11 @@ dompurify@^2.4.1:
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.1.tgz#f9cb1a275fde9af6f2d0a2644ef648dd6847b631"
|
||||
integrity sha512-ewwFzHzrrneRjxzmK6oVz/rZn9VWspGFRDb4/rRtIsM1n36t9AKma/ye8syCpcw+XJ25kOK/hOG7t1j2I2yBqA==
|
||||
|
||||
dompurify@^2.4.3:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.3.tgz#f4133af0e6a50297fc8874e2eaedc13a3c308c03"
|
||||
integrity sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==
|
||||
|
||||
domutils@^2.0.0, domutils@^2.5.2:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.7.0.tgz#8ebaf0c41ebafcf55b0b72ec31c56323712c5442"
|
||||
@ -14783,6 +14919,15 @@ fs-extra@^10.0.0:
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
|
||||
fs-extra@^11.1.0:
|
||||
version "11.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.0.tgz#5784b102104433bb0e090f48bfc4a30742c357ed"
|
||||
integrity sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.0"
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
|
||||
fs-extra@^9.0.0, fs-extra@^9.0.1, fs-extra@^9.1.0:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
|
||||
@ -18414,6 +18559,17 @@ linkedom@^0.14.16:
|
||||
htmlparser2 "^8.0.1"
|
||||
uhyphen "^0.1.0"
|
||||
|
||||
linkedom@^0.14.21:
|
||||
version "0.14.21"
|
||||
resolved "https://registry.yarnpkg.com/linkedom/-/linkedom-0.14.21.tgz#878e1e5e88028cb1d57bc6262f84484a41a37497"
|
||||
integrity sha512-V+c0AAFMTVJA2iAhrdd+u44lL0TjL6hBenVB061VQ6BHqTAHtXw1v5F1/CHGKtwg0OHm+hrGbepb9ZSFJ7lJkg==
|
||||
dependencies:
|
||||
css-select "^5.1.0"
|
||||
cssom "^0.5.0"
|
||||
html-escaper "^3.0.3"
|
||||
htmlparser2 "^8.0.1"
|
||||
uhyphen "^0.1.0"
|
||||
|
||||
linkedom@^0.14.9:
|
||||
version "0.14.9"
|
||||
resolved "https://registry.yarnpkg.com/linkedom/-/linkedom-0.14.9.tgz#34c6f15eddc809406f42d8ee48cd30b0222eccb0"
|
||||
@ -25566,6 +25722,11 @@ tr46@~0.0.3:
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
||||
|
||||
"traverse@>=0.3.0 <0.4":
|
||||
version "0.3.9"
|
||||
resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"
|
||||
integrity sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==
|
||||
|
||||
tree-kill@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
|
||||
@ -26151,6 +26312,14 @@ untildify@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
|
||||
integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
|
||||
|
||||
unzip-stream@^0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/unzip-stream/-/unzip-stream-0.3.1.tgz#2333b5cd035d29db86fb701ca212cf8517400083"
|
||||
integrity sha512-RzaGXLNt+CW+T41h1zl6pGz3EaeVhYlK+rdAap+7DxW5kqsqePO8kRtWPaCiVqdhZc86EctSPVYNix30YOMzmw==
|
||||
dependencies:
|
||||
binary "^0.3.0"
|
||||
mkdirp "^0.5.1"
|
||||
|
||||
upath@^1.1.1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
|
||||
|
||||
Reference in New Issue
Block a user