Merge pull request #43 from payloadcms/fix/add-file-to-local-operations

fix: add file to create and update local operations
This commit is contained in:
James Mikrut
2021-02-05 15:16:03 -06:00
committed by GitHub
12 changed files with 128 additions and 4 deletions

View File

@@ -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'),
})
```

View File

@@ -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.

View File

@@ -102,6 +102,7 @@
"lodash.merge": "^4.6.2",
"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",

View File

@@ -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}`);

View File

@@ -1,4 +1,5 @@
import { Document } from '../../../types';
import getFileByPath from '../../../uploads/getFileByPath';
export type Options = {
collection: string
@@ -10,8 +11,8 @@ export type Options = {
overrideAccess?: boolean
disableVerificationEmail?: boolean
showHiddenFields?: boolean
filePath?: string
}
export default async function create(options: Options): Promise<Document> {
const {
collection: collectionSlug,
@@ -23,6 +24,7 @@ export default async function create(options: Options): Promise<Document> {
overrideAccess = true,
disableVerificationEmail,
showHiddenFields,
filePath,
} = options;
const collection = this.collections[collectionSlug];
@@ -40,6 +42,7 @@ export default async function create(options: Options): Promise<Document> {
locale,
fallbackLocale,
payload: this,
file: getFileByPath(filePath),
},
});
}

View 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);
});
});
});

View File

@@ -1,4 +1,5 @@
import { Document } from '../../../types';
import getFileByPath from '../../../uploads/getFileByPath';
export type Options = {
collection: string
@@ -10,6 +11,7 @@ export type Options = {
user?: Document
overrideAccess?: boolean
showHiddenFields?: boolean
filePath?: string
}
export default async function update(options: Options): Promise<Document> {
@@ -23,6 +25,7 @@ export default async function update(options: Options): Promise<Document> {
user,
overrideAccess = true,
showHiddenFields,
filePath,
} = options;
const collection = this.collections[collectionSlug];
@@ -40,6 +43,7 @@ export default async function update(options: Options): Promise<Document> {
locale,
fallbackLocale,
payload: this,
file: getFileByPath(filePath),
},
};

View File

@@ -19,6 +19,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
@@ -199,7 +200,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;

View 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;

View 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;

View File

@@ -41,3 +41,9 @@ export type Upload = {
staticDir: string
adminThumbnail?: string
}
export type File = {
data: Buffer
mimetype: string
name: string
}

View File

@@ -8567,6 +8567,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"