diff --git a/packages/api/src/datalayer/upload_files/model.ts b/packages/api/src/datalayer/upload_files/model.ts index c6a2055d8..49db669e2 100644 --- a/packages/api/src/datalayer/upload_files/model.ts +++ b/packages/api/src/datalayer/upload_files/model.ts @@ -46,7 +46,7 @@ export const createKeys = exclude(keys, defaultedKeys) export type CreateSet = PickTuple & Partialize -export const updateKeys = ['status'] as const +export const updateKeys = ['url', 'status'] as const export type UpdateSet = PickTuple diff --git a/packages/api/src/resolvers/upload_files/index.ts b/packages/api/src/resolvers/upload_files/index.ts index d0888c3f8..d6c9684e5 100644 --- a/packages/api/src/resolvers/upload_files/index.ts +++ b/packages/api/src/resolvers/upload_files/index.ts @@ -11,6 +11,7 @@ import { WithDataSourcesContext } from '../types' import { generateUploadSignedUrl, generateUploadFilePathName, + getFilePublicUrl, } from '../../utils/uploads' import path from 'path' import normalizeUrl from 'normalize-url' @@ -19,6 +20,12 @@ import { env } from '../../env' import { createPage, getPageByParam, updatePage } from '../../elastic/pages' import { PageType } from '../../elastic/types' import { generateSlug } from '../../utils/helpers' +import { validateUrl } from '../../services/create_page_save_request' + +const isFileUrl = (url: string): boolean => { + const parsedUrl = new URL(url) + return parsedUrl.protocol == 'file:' +} export const uploadFileRequestResolver: ResolverFn< UploadFileRequestResult, @@ -60,6 +67,17 @@ export const uploadFileRequestResolver: ResolverFn< if (!fileName) { fileName = 'content.pdf' } + + if (!isFileUrl(url)) { + try { + validateUrl(url) + } catch (error) { + console.log('illegal file input url', error) + return { + errorCodes: [UploadFileRequestErrorCode.BadInput], + } + } + } } catch { return { errorCodes: [UploadFileRequestErrorCode.BadInput] } } @@ -82,12 +100,28 @@ export const uploadFileRequestResolver: ResolverFn< input.contentType ) + const publicUrl = getFilePublicUrl(uploadFilePathName) + + // If this is a file URL, we swap in the GCS public URL + if (isFileUrl(input.url)) { + await models.uploadFile.update(uploadFileData.id, { + url: publicUrl, + status: UploadFileStatus.Initialized, + }) + } + let createdPageId: string | undefined = undefined if (input.createPageEntry) { - const page = await getPageByParam({ - userId: claims.uid, - url: input.url, - }) + // If we have a file:// URL, don't try to match it + // and create a copy of the page, just create a + // new item. + const page = isFileUrl(input.url) + ? await getPageByParam({ + userId: claims.uid, + url: input.url, + }) + : undefined + if (page) { if ( !(await updatePage( @@ -105,7 +139,7 @@ export const uploadFileRequestResolver: ResolverFn< } else { const pageId = await createPage( { - url: input.url, + url: isFileUrl(input.url) ? publicUrl : input.url, id: input.clientRequestId || '', userId: claims.uid, title: title, diff --git a/packages/api/src/utils/uploads.ts b/packages/api/src/utils/uploads.ts index 2fc5535d6..6c4492479 100644 --- a/packages/api/src/utils/uploads.ts +++ b/packages/api/src/utils/uploads.ts @@ -15,6 +15,10 @@ const storage = env.fileUpload?.gcsUploadSAKeyFilePath : new Storage() const bucketName = env.fileUpload.gcsUploadBucket +export const getFilePublicUrl = (filePathName: string): string => { + return storage.bucket(bucketName).file(filePathName).publicUrl() +} + export const generateUploadSignedUrl = async ( filePathName: string, contentType: string, diff --git a/packages/api/test/resolvers/upload_file_request.test.ts b/packages/api/test/resolvers/upload_file_request.test.ts new file mode 100644 index 000000000..2ec593195 --- /dev/null +++ b/packages/api/test/resolvers/upload_file_request.test.ts @@ -0,0 +1,107 @@ +import { createTestUser, deleteTestUser } from '../db' +import { + generateFakeUuid, + graphqlRequest, + request, +} from '../util' +import * as chai from 'chai' +import { expect } from 'chai' +import 'mocha' +import { User } from '../../src/entity/user' +import chaiString from 'chai-string' +import { + PageContext, +} from '../../src/elastic/types' +import { createPubSubClient } from '../../src/datalayer/pubsub' +import { + deletePage, + getPageById, +} from '../../src/elastic/pages' + +chai.use(chaiString) + +// INPUT +// clientRequestId?: InputMaybe; +// contentType: Scalars['String']; +// createPageEntry?: InputMaybe; +// url: Scalars['String']; + +const uploadFileRequest = async ( + authToken: string, + inputUrl: string, + clientRequestId: string, + createPageEntry = true + ) => { + const query = ` + mutation { + uploadFileRequest( + input: { + contentType: "application/pdf", + clientRequestId: "${clientRequestId}", + createPageEntry: ${createPageEntry}, + url: "${inputUrl}" + } + ) { + ... on ArchiveLinkSuccess { + linkId + } + ... on ArchiveLinkError { + errorCodes + } + } + } + ` + return graphqlRequest(query, authToken).expect(200) +} + +describe('uploadFileRequest API', () => { + const username = 'fakeUser' + let authToken: string + let user: User + let ctx: PageContext + + before(async () => { + // create test user and login + user = await createTestUser(username) + const res = await request + .post('/local/debug/fake-user-login') + .send({ fakeEmail: user.email }) + + authToken = res.body.authToken + + ctx = { + pubsub: createPubSubClient(), + refresh: true, + uid: user.id, + } + }) + + after(async () => { + await deleteTestUser(username) + }) + + describe('UploadFileRequest', () => { + context('when create article is true', () => { + const clientRequestId = generateFakeUuid() + + after(async () => { + await deletePage(clientRequestId, ctx) + }) + + it('should create an article if create article is true', async () => { + const res = await uploadFileRequest(authToken, 'https://www.google.com', clientRequestId, true) + expect(res.body.data.uploadFileRequest.createdPageId).to.eql(clientRequestId) + const page = await getPageById(clientRequestId) + expect(page).to.be + }) + + it('should not save a file:// URL', async () => { + const res = await uploadFileRequest(authToken, 'file://foo.bar', clientRequestId, true) + expect(res.body.data.uploadFileRequest.createdPageId).to.eql(clientRequestId) + const page = await getPageById(clientRequestId) + expect(page?.url).to.startWith("https://") + }) + }) + }) +}) +