feat: allows upload through Local API
This commit is contained in:
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -14,7 +14,7 @@
|
||||
"--runInBand"
|
||||
],
|
||||
"env": {
|
||||
"PAYLOAD_CONFIG_PATH": "demo/payload.config.js"
|
||||
"PAYLOAD_CONFIG_PATH": "demo/payload.config.ts"
|
||||
},
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
|
||||
@@ -96,6 +96,11 @@ const post = await payload.create({
|
||||
// If creating verification-enabled auth doc,
|
||||
// you can optionally disable the email that is auto-sent
|
||||
disableVerificationEmail: true,
|
||||
|
||||
// If your collection supports uploads, you can upload
|
||||
// a file directly through the Local API by providing
|
||||
// its full, absolute file path.
|
||||
filePath: path.resolve(__dirname, './path-to-image.jpg'),
|
||||
})
|
||||
```
|
||||
|
||||
@@ -152,6 +157,11 @@ const result = await payload.update({
|
||||
user: dummyUser,
|
||||
overrideAccess: false,
|
||||
showHiddenFields: true,
|
||||
|
||||
// If your collection supports uploads, you can upload
|
||||
// a file directly through the Local API by providing
|
||||
// its full, absolute file path.
|
||||
filePath: path.resolve(__dirname, './path-to-image.jpg'),
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ Behind the scenes, Payload relies on [`sharp`](https://sharp.pixelplumbing.com/a
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Important:</strong><br/>
|
||||
Uploading files is currently only possible through the REST API due to how GraphQL works. It's difficult and fairly nonsensical to support uploading files through GraphQL.
|
||||
Uploading files is currently only possible through the REST and Local APIs due to how GraphQL works. It's difficult and fairly nonsensical to support uploading files through GraphQL.
|
||||
</Banner>
|
||||
|
||||
To upload a file, use your collection's [`create`](/docs/rest-api/overview#collections) endpoint. Send it all the data that your Collection requires, as well as a `file` key containing the file that you'd like to upload.
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"method-override": "^3.0.0",
|
||||
"micro-memoize": "^4.0.9",
|
||||
"mime": "^2.5.0",
|
||||
"mini-css-extract-plugin": "1.3.3",
|
||||
"minimist": "^1.2.0",
|
||||
"mkdirp": "^1.0.4",
|
||||
|
||||
@@ -18,6 +18,7 @@ import { AfterChangeHook, BeforeOperationHook, BeforeValidateHook, Collection }
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { Document } from '../../types';
|
||||
import { Payload } from '../..';
|
||||
import saveBufferToFile from '../../uploads/saveBufferToFile';
|
||||
|
||||
export type Arguments = {
|
||||
collection: Collection
|
||||
@@ -152,7 +153,7 @@ async function create(this: Payload, incomingArgs: Arguments): Promise<Document>
|
||||
const fsSafeName = await getSafeFilename(staticPath, file.name);
|
||||
|
||||
try {
|
||||
await file.mv(`${staticPath}/${fsSafeName}`);
|
||||
await saveBufferToFile(file.data, `${staticPath}/${fsSafeName}`);
|
||||
|
||||
if (isImage(file.mimetype)) {
|
||||
const dimensions = await getImageSize(`${staticPath}/${fsSafeName}`);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { UploadedFile } from 'express-fileupload';
|
||||
import { Document } from '../../../types';
|
||||
import getFileByPath from '../../../uploads/getFileByPath';
|
||||
|
||||
export type Options = {
|
||||
collection: string
|
||||
@@ -11,9 +11,8 @@ export type Options = {
|
||||
overrideAccess?: boolean
|
||||
disableVerificationEmail?: boolean
|
||||
showHiddenFields?: boolean
|
||||
file?: UploadedFile
|
||||
filePath?: string
|
||||
}
|
||||
|
||||
export default async function create(options: Options): Promise<Document> {
|
||||
const {
|
||||
collection: collectionSlug,
|
||||
@@ -25,7 +24,7 @@ export default async function create(options: Options): Promise<Document> {
|
||||
overrideAccess = true,
|
||||
disableVerificationEmail,
|
||||
showHiddenFields,
|
||||
file,
|
||||
filePath,
|
||||
} = options;
|
||||
|
||||
const collection = this.collections[collectionSlug];
|
||||
@@ -43,7 +42,7 @@ export default async function create(options: Options): Promise<Document> {
|
||||
locale,
|
||||
fallbackLocale,
|
||||
payload: this,
|
||||
file,
|
||||
file: getFileByPath(filePath),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
50
src/collections/operations/local/local.spec.js
Normal file
50
src/collections/operations/local/local.spec.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import path from 'path';
|
||||
import payload from '../../..';
|
||||
|
||||
let createdMediaID;
|
||||
|
||||
payload.init({
|
||||
secret: 'SECRET_KEY',
|
||||
mongoURL: 'mongodb://localhost/payload',
|
||||
local: true,
|
||||
});
|
||||
|
||||
describe('Collections - Local', () => {
|
||||
describe('Create', () => {
|
||||
it('should allow an upload-enabled file to be created and uploaded', async () => {
|
||||
const alt = 'Alt Text Here';
|
||||
|
||||
const result = await payload.create({
|
||||
collection: 'media',
|
||||
data: {
|
||||
alt,
|
||||
},
|
||||
filePath: path.resolve(__dirname, '../../../admin/assets/images/generic-block-image.svg'),
|
||||
});
|
||||
|
||||
expect(result.id).not.toBeNull();
|
||||
expect(result.alt).toStrictEqual(alt);
|
||||
expect(result.filename).toStrictEqual('generic-block-image.svg');
|
||||
createdMediaID = result.id;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Update', () => {
|
||||
it('should allow an upload-enabled file to be re-uploaded and alt-text to be changed.', async () => {
|
||||
const newAltText = 'New Alt Text Here';
|
||||
|
||||
const result = await payload.update({
|
||||
collection: 'media',
|
||||
id: createdMediaID,
|
||||
data: {
|
||||
alt: newAltText,
|
||||
},
|
||||
filePath: path.resolve(__dirname, '../../../admin/assets/images/og-image.png'),
|
||||
});
|
||||
|
||||
expect(result.alt).toStrictEqual(newAltText);
|
||||
expect(result.sizes.mobile.width).toStrictEqual(320);
|
||||
expect(result.width).toStrictEqual(640);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { UploadedFile } from 'express-fileupload';
|
||||
import { Document } from '../../../types';
|
||||
import getFileByPath from '../../../uploads/getFileByPath';
|
||||
|
||||
export type Options = {
|
||||
collection: string
|
||||
@@ -11,7 +11,7 @@ export type Options = {
|
||||
user?: Document
|
||||
overrideAccess?: boolean
|
||||
showHiddenFields?: boolean
|
||||
file?: UploadedFile
|
||||
filePath?: string
|
||||
}
|
||||
|
||||
export default async function update(options: Options): Promise<Document> {
|
||||
@@ -25,7 +25,7 @@ export default async function update(options: Options): Promise<Document> {
|
||||
user,
|
||||
overrideAccess = true,
|
||||
showHiddenFields,
|
||||
file,
|
||||
filePath,
|
||||
} = options;
|
||||
|
||||
const collection = this.collections[collectionSlug];
|
||||
@@ -43,7 +43,7 @@ export default async function update(options: Options): Promise<Document> {
|
||||
locale,
|
||||
fallbackLocale,
|
||||
payload: this,
|
||||
file,
|
||||
file: getFileByPath(filePath),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { FileData } from '../../uploads/types';
|
||||
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { hasWhereAccessResult, UserDocument } from '../../auth/types';
|
||||
import saveBufferToFile from '../../uploads/saveBufferToFile';
|
||||
|
||||
export type Arguments = {
|
||||
collection: Collection
|
||||
@@ -198,7 +199,7 @@ async function update(incomingArgs: Arguments): Promise<Document> {
|
||||
const fsSafeName = await getSafeFilename(staticPath, file.name);
|
||||
|
||||
try {
|
||||
await file.mv(`${staticPath}/${fsSafeName}`);
|
||||
await saveBufferToFile(file.data, `${staticPath}/${fsSafeName}`);
|
||||
|
||||
fileData.filename = fsSafeName;
|
||||
fileData.filesize = file.size;
|
||||
|
||||
22
src/uploads/getFileByPath.ts
Normal file
22
src/uploads/getFileByPath.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import fs from 'fs';
|
||||
import mime from 'mime';
|
||||
import { File } from './types';
|
||||
|
||||
const getFileByPath = (filePath: string): File => {
|
||||
if (typeof filePath === 'string') {
|
||||
const data = fs.readFileSync(filePath);
|
||||
const mimetype = mime.getType(filePath);
|
||||
|
||||
const name = filePath.split('/').pop();
|
||||
|
||||
return {
|
||||
data,
|
||||
mimetype,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export default getFileByPath;
|
||||
21
src/uploads/saveBufferToFile.ts
Normal file
21
src/uploads/saveBufferToFile.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Readable } from 'stream';
|
||||
import fs from 'fs';
|
||||
|
||||
/**
|
||||
* Save buffer data to a file.
|
||||
* @param {Buffer} buffer - buffer to save to a file.
|
||||
* @param {string} filePath - path to a file.
|
||||
*/
|
||||
const saveBufferToFile = async (buffer: Buffer, filePath: string): Promise<void> => {
|
||||
// Setup readable stream from buffer.
|
||||
let streamData = buffer;
|
||||
const readStream = new Readable();
|
||||
readStream._read = () => {
|
||||
readStream.push(streamData);
|
||||
streamData = null;
|
||||
};
|
||||
// Setup file system writable stream.
|
||||
return fs.writeFileSync(filePath, buffer);
|
||||
};
|
||||
|
||||
export default saveBufferToFile;
|
||||
@@ -41,3 +41,9 @@ export type Upload = {
|
||||
staticDir: string
|
||||
adminThumbnail?: string
|
||||
}
|
||||
|
||||
export type File = {
|
||||
data: Buffer
|
||||
mimetype: string
|
||||
name: string
|
||||
}
|
||||
|
||||
@@ -8562,6 +8562,11 @@ mime@^2.3.1:
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.7.tgz#962aed9be0ed19c91fd7dc2ece5d7f4e89a90d74"
|
||||
integrity sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==
|
||||
|
||||
mime@^2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.0.tgz#2b4af934401779806ee98026bb42e8c1ae1876b1"
|
||||
integrity sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag==
|
||||
|
||||
mimic-fn@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
|
||||
|
||||
Reference in New Issue
Block a user