mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
add pdf attachment upload
This commit is contained in:
parent
3f34813e97
commit
5d95e6c734
4 changed files with 228 additions and 1 deletions
120
packages/api/src/routers/svc/pdf_attachments.ts
Normal file
120
packages/api/src/routers/svc/pdf_attachments.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import express from 'express'
|
||||
import { env } from '../../env'
|
||||
import * as jwt from 'jsonwebtoken'
|
||||
import { PageType, UploadFileStatus } from '../../generated/graphql'
|
||||
import {
|
||||
generateUploadFilePathName,
|
||||
generateUploadSignedUrl,
|
||||
getStorageFileDetails,
|
||||
} from '../../utils/uploads'
|
||||
import { initModels } from '../../server'
|
||||
import { kx } from '../../datalayer/knex_config'
|
||||
import { analytics } from '../../utils/analytics'
|
||||
|
||||
export function pdfAttachmentsRouter() {
|
||||
const router = express.Router()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
router.post('/upload', async (req, res) => {
|
||||
console.log('pdf-attachments/upload')
|
||||
|
||||
const { email, fileName } = req.body as {
|
||||
email: string
|
||||
fileName: string
|
||||
}
|
||||
|
||||
const token = req?.headers?.authorization
|
||||
if (!token || !jwt.verify(token, env.server.jwtSecret)) {
|
||||
return res.status(401).send('UNAUTHORIZED')
|
||||
}
|
||||
|
||||
const models = initModels(kx, false)
|
||||
const user = await models.user.getWhere({ email })
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).send('UNAUTHORIZED')
|
||||
}
|
||||
|
||||
analytics.track({
|
||||
userId: user.id,
|
||||
event: 'pdf-attachment-upload',
|
||||
properties: {
|
||||
env: env.server.apiEnv,
|
||||
},
|
||||
})
|
||||
|
||||
const contentType = 'application/pdf'
|
||||
const uploadFileData = await models.uploadFile.create({
|
||||
url: '',
|
||||
userId: user.id,
|
||||
fileName: fileName,
|
||||
status: UploadFileStatus.Initialized,
|
||||
contentType: contentType,
|
||||
})
|
||||
|
||||
if (uploadFileData.id) {
|
||||
const uploadFilePathName = generateUploadFilePathName(
|
||||
uploadFileData.id,
|
||||
fileName
|
||||
)
|
||||
const uploadSignedUrl = await generateUploadSignedUrl(
|
||||
uploadFilePathName,
|
||||
contentType
|
||||
)
|
||||
res.send({
|
||||
id: uploadFileData.id,
|
||||
url: uploadSignedUrl,
|
||||
})
|
||||
} else {
|
||||
res.status(400).send('BAD REQUEST')
|
||||
}
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
router.post('create-article', async (req, res) => {
|
||||
console.log('pdf-attachments/create-article')
|
||||
|
||||
const { email, uploadFileId } = req.body as {
|
||||
email: string
|
||||
uploadFileId: string
|
||||
}
|
||||
|
||||
const token = req?.headers?.authorization
|
||||
if (!token || !jwt.verify(token, env.server.jwtSecret)) {
|
||||
return res.status(401).send('UNAUTHORIZED')
|
||||
}
|
||||
|
||||
const models = initModels(kx, false)
|
||||
const user = await models.user.getWhere({ email })
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).send('UNAUTHORIZED')
|
||||
}
|
||||
|
||||
analytics.track({
|
||||
userId: user.id,
|
||||
event: 'create-article',
|
||||
properties: {
|
||||
env: env.server.apiEnv,
|
||||
},
|
||||
})
|
||||
|
||||
const uploadFile = await models.uploadFile.getWhere({
|
||||
id: uploadFileId,
|
||||
userId: user.id,
|
||||
})
|
||||
if (!uploadFile) {
|
||||
return res.status(400).send('BAD REQUEST')
|
||||
}
|
||||
const uploadFileDetails = await getStorageFileDetails(
|
||||
uploadFileId,
|
||||
uploadFile.fileName
|
||||
)
|
||||
const uploadFileHash = uploadFileDetails.md5Hash
|
||||
const userArticleUrl = uploadFileDetails.fileUrl
|
||||
const canonicalUrl = uploadFile.url
|
||||
const pageType = PageType.File
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ import { emailsServiceRouter } from './routers/svc/emails'
|
|||
import ReminderModel from './datalayer/reminders'
|
||||
import { remindersServiceRouter } from './routers/svc/reminders'
|
||||
import { ApolloServer } from 'apollo-server-express'
|
||||
import { pdfAttachmentsRouter } from './routers/svc/pdf_attachments'
|
||||
|
||||
const PORT = process.env.PORT || 4000
|
||||
|
||||
|
|
@ -97,6 +98,7 @@ export const createApp = (): {
|
|||
app.use('/svc/pubsub/newsletters', newsletterServiceRouter())
|
||||
app.use('/svc/pubsub/emails', emailsServiceRouter())
|
||||
app.use('/svc/reminders', remindersServiceRouter())
|
||||
app.use('/svc/pdf-attachments', pdfAttachmentsRouter())
|
||||
|
||||
if (env.dev.isLocal) {
|
||||
app.use('/local/debug', localDebugRouter())
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
isNewsletter,
|
||||
} from './newsletter'
|
||||
import { PubSub } from '@google-cloud/pubsub'
|
||||
import { handlePdfAttachment } from './pdf'
|
||||
|
||||
const NON_NEWSLETTER_EMAIL_TOPIC = 'nonNewsletterEmailReceived'
|
||||
const pubsub = new PubSub()
|
||||
|
|
@ -23,9 +24,14 @@ export const inboundEmailHandler = Sentry.GCPFunction.wrapHttpFunction(
|
|||
const parsed: Record<string, string> = {}
|
||||
|
||||
for (const part of parts) {
|
||||
const { name, data } = part
|
||||
const { name, data, type, filename } = part
|
||||
if (name && data) {
|
||||
parsed[name] = data.toString()
|
||||
} else if (type === 'application/pdf' && data) {
|
||||
parsed['pdf-attachment-data'] = data.toString()
|
||||
parsed['pdf-attachment-filename'] = filename
|
||||
? filename
|
||||
: 'attachment.pdf'
|
||||
} else {
|
||||
console.log('no data or name for ', part)
|
||||
}
|
||||
|
|
@ -76,7 +82,15 @@ export const inboundEmailHandler = Sentry.GCPFunction.wrapHttpFunction(
|
|||
if (isConfirmationEmail(from)) {
|
||||
console.log('handleConfirmation', from, recipientAddress)
|
||||
await handleConfirmation(recipientAddress, subject)
|
||||
} else if (parsed['pdf-attachment']) {
|
||||
console.log('handle PDF attachment', from, recipientAddress)
|
||||
await handlePdfAttachment(
|
||||
recipientAddress,
|
||||
parsed['pdf-attachment-filename'],
|
||||
parsed['pdf-attachment-data']
|
||||
)
|
||||
}
|
||||
|
||||
// queue non-newsletter emails
|
||||
await pubsub.topic(NON_NEWSLETTER_EMAIL_TOPIC).publishMessage({
|
||||
json: {
|
||||
|
|
|
|||
91
packages/inbound-email-handler/src/pdf.ts
Normal file
91
packages/inbound-email-handler/src/pdf.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import axios, { AxiosResponse } from 'axios'
|
||||
import { promisify } from 'util'
|
||||
import * as jwt from 'jsonwebtoken'
|
||||
|
||||
const signToken = promisify(jwt.sign)
|
||||
|
||||
type UploadResponse = {
|
||||
id: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export const handlePdfAttachment = async (
|
||||
email: string,
|
||||
fileName: string,
|
||||
data: string
|
||||
): Promise<void> => {
|
||||
const uploadResult = await getUploadIdAndSignedUrl(email, fileName)
|
||||
await uploadToSignedUrl(uploadResult.url, data)
|
||||
await createArticle(email, uploadResult.id)
|
||||
}
|
||||
|
||||
const getUploadIdAndSignedUrl = async (
|
||||
email: string,
|
||||
fileName: string
|
||||
): Promise<UploadResponse> => {
|
||||
if (process.env.JWT_SECRET === undefined) {
|
||||
throw new Error('JWT_SECRET is not defined')
|
||||
}
|
||||
const auth = await signToken(email, process.env.JWT_SECRET)
|
||||
const data = {
|
||||
fileName,
|
||||
email,
|
||||
}
|
||||
|
||||
if (process.env.REST_BACKEND_ENDPOINT === undefined) {
|
||||
throw new Error('REST_BACKEND_ENDPOINT is not defined')
|
||||
}
|
||||
const response = await axios.post(
|
||||
`${process.env.REST_BACKEND_ENDPOINT}/svc/pdf-attachments/upload`,
|
||||
data,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `${auth as string}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
return response.data as UploadResponse
|
||||
}
|
||||
|
||||
const uploadToSignedUrl = (
|
||||
uploadUrl: string,
|
||||
data: string
|
||||
): Promise<AxiosResponse> => {
|
||||
return axios.put(uploadUrl, data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
},
|
||||
maxBodyLength: 1000000000,
|
||||
maxContentLength: 100000000,
|
||||
})
|
||||
}
|
||||
|
||||
const createArticle = async (
|
||||
email: string,
|
||||
uploadFileId: string
|
||||
): Promise<AxiosResponse> => {
|
||||
const data = {
|
||||
email,
|
||||
uploadFileId,
|
||||
}
|
||||
|
||||
if (process.env.JWT_SECRET === undefined) {
|
||||
throw new Error('JWT_SECRET is not defined')
|
||||
}
|
||||
const auth = await signToken(email, process.env.JWT_SECRET)
|
||||
|
||||
if (process.env.REST_BACKEND_ENDPOINT === undefined) {
|
||||
throw new Error('REST_BACKEND_ENDPOINT is not defined')
|
||||
}
|
||||
return axios.post(
|
||||
`${process.env.REST_BACKEND_ENDPOINT}/svc/pdf-attachments/create-article`,
|
||||
data,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `${auth as string}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue