allow downloading/uploading readable content

This commit is contained in:
Hongbo Wu 2024-05-15 21:46:22 +08:00
parent 854ca08142
commit 8cfa24a847
7 changed files with 84 additions and 35 deletions

View file

@ -186,12 +186,12 @@ export const savePageJob = async (data: Data, attemptsMade: number) => {
}
// download the original content
const filePath = contentFilePath(
const filePath = contentFilePath({
userId,
articleSavingRequestId,
new Date(savedAt).getTime(),
'original'
)
libraryItemId: articleSavingRequestId,
format: 'original',
savedAt: new Date(savedAt),
})
const exists = await isFileExists(filePath)
if (!exists) {
logger.error('Original content file does not exist', {

View file

@ -2,11 +2,15 @@ import { Highlight } from '../entity/highlight'
import { findLibraryItemById } from '../services/library_item'
import { logger } from '../utils/logger'
import { htmlToHighlightedMarkdown, htmlToMarkdown } from '../utils/parser'
import { uploadToBucket } from '../utils/uploads'
import { isFileExists, uploadToBucket } from '../utils/uploads'
export const UPLOAD_CONTENT_JOB = 'UPLOAD_CONTENT_JOB'
export type ContentFormat = 'markdown' | 'highlightedMarkdown' | 'original'
export type ContentFormat =
| 'markdown'
| 'highlightedMarkdown'
| 'original'
| 'readable'
export interface UploadContentJobData {
libraryItemId: string
@ -26,6 +30,7 @@ const convertContent = (
case 'highlightedMarkdown':
return htmlToHighlightedMarkdown(content, highlights)
case 'original':
case 'readable':
return content
default:
throw new Error('Unsupported format')
@ -36,6 +41,7 @@ const CONTENT_TYPES = {
markdown: 'text/markdown',
highlightedMarkdown: 'text/markdown',
original: 'text/html',
readable: 'text/html',
}
const getSelectOptions = (
@ -43,6 +49,7 @@ const getSelectOptions = (
): { column: 'readableContent' | 'originalContent'; highlights?: boolean } => {
switch (format) {
case 'markdown':
case 'readable':
return {
column: 'readableContent',
}
@ -73,31 +80,40 @@ export const uploadContentJob = async (data: UploadContentJobData) => {
},
})
if (!libraryItem) {
logger.error('Library item not found', data)
logger.error(`Library item not found: ${libraryItemId}`)
throw new Error('Library item not found')
}
const content = libraryItem[column]
if (!content) {
logger.error(`${column} not found`, data)
logger.error(`${column} not found`)
throw new Error('Content not found')
}
logger.info('Converting content', data)
logger.info('Converting content')
const convertedContent = convertContent(
content,
format,
libraryItem.highlights
)
console.time('uploadToBucket')
logger.info('Uploading content', data)
const exists = await isFileExists(filePath)
if (exists) {
logger.info(`File already exists: ${filePath}`)
return
}
logger.info(`Uploading content: ${filePath}`)
logger.profile('Uploader')
await uploadToBucket(filePath, Buffer.from(convertedContent), {
contentType: CONTENT_TYPES[format],
timeout: 60000, // 1 minute
timeout: 10_000, // 10 seconds
})
console.timeEnd('uploadToBucket')
logger.info('Content uploaded', data)
logger.profile('Uploader', {
level: 'info',
message: 'Content uploaded',
})
}

View file

@ -72,14 +72,13 @@ export function contentRouter() {
// generate signed url for each library item
const data = await Promise.all(
libraryItems.map(async (libraryItem) => {
const date =
format === 'original' ? libraryItem.savedAt : libraryItem.updatedAt
const filePath = contentFilePath(
const filePath = contentFilePath({
userId,
libraryItem.id,
date.getTime(),
format
)
libraryItemId: libraryItem.id,
format,
savedAt: libraryItem.savedAt,
updatedAt: libraryItem.updatedAt,
})
try {
const downloadUrl = await generateDownloadSignedUrl(filePath, {
@ -89,7 +88,7 @@ export function contentRouter() {
// check if file is already uploaded
const exists = await isFileExists(filePath)
if (exists) {
logger.info('File already exists', filePath)
logger.info(`File already exists: ${filePath}`)
}
return {
@ -109,7 +108,10 @@ export function contentRouter() {
}
})
)
logger.info('Signed urls generated', data)
logger.info(
'Signed urls generated',
data.map((d) => d.downloadUrl)
)
// skip uploading if there is an error or file already exists
const uploadData = data.filter(
@ -117,8 +119,8 @@ export function contentRouter() {
) as UploadContentJobData[]
if (uploadData.length > 0) {
await enqueueBulkUploadContentJob(uploadData)
logger.info('Bulk upload content job enqueued', uploadData)
const jobs = await enqueueBulkUploadContentJob(uploadData)
logger.info('Bulk upload content job enqueued', jobs)
}
res.send({

View file

@ -1695,14 +1695,21 @@ export const uploadOriginalContent = async (
userId: string,
libraryItemId: string,
savedAt: Date,
originalContent: string
originalContent: string,
timeout = 10_000 // 10 seconds
) => {
await uploadToBucket(
contentFilePath(userId, libraryItemId, savedAt.getTime(), 'original'),
contentFilePath({
userId,
libraryItemId,
savedAt,
format: 'original',
}),
Buffer.from(originalContent),
{
public: false,
contentType: 'text/html',
timeout,
}
)
}
@ -1713,6 +1720,11 @@ export const downloadOriginalContent = async (
savedAt: Date
) => {
return downloadFromBucket(
contentFilePath(userId, libraryItemId, savedAt.getTime(), 'original')
contentFilePath({
userId,
libraryItemId,
savedAt,
format: 'original',
})
)
}

View file

@ -970,6 +970,9 @@ export const enqueueBulkUploadContentJob = async (
name: UPLOAD_CONTENT_JOB,
data: d,
opts: {
jobId: `${UPLOAD_CONTENT_JOB}_${d.filePath}_${JOB_VERSION}`, // dedupe by job id
removeOnComplete: true,
removeOnFail: true,
attempts: 3,
priority: getJobPriority(UPLOAD_CONTENT_JOB),
},

View file

@ -49,7 +49,7 @@ type FillNodeResponse = {
}
function getTextNodesBetween(rootNode: Node, startNode: Node, endNode: Node) {
const maxTime = 1000 * 60 // 60 seconds
const maxTime = 10_000 // 10 seconds
const start = Date.now()
let textNodeStartingPoint = 0
let articleText = ''

View file

@ -163,9 +163,25 @@ export const downloadFromBucket = async (filePath: string): Promise<Buffer> => {
return data
}
export const contentFilePath = (
userId: string,
libraryItemId: string,
timestamp: number,
export const contentFilePath = ({
userId,
libraryItemId,
format,
savedAt,
updatedAt,
}: {
userId: string
libraryItemId: string
format: ContentFormat
) => `content/${userId}/${libraryItemId}.${timestamp}.${format}`
savedAt?: Date
updatedAt?: Date
}) => {
// Use updatedAt for highlightedMarkdown format because highlights are saved
const date = format === 'highlightedMarkdown' ? updatedAt : savedAt
if (!date) {
throw new Error('Date not found')
}
return `content/${userId}/${libraryItemId}.${date.getTime()}.${format}`
}