From 609bbe58660d183a14b8b2eda1e9c9a5fec09d6d Mon Sep 17 00:00:00 2001 From: DrMint <29893320+DrMint@users.noreply.github.com> Date: Thu, 4 Apr 2024 21:42:48 +0200 Subject: [PATCH] Added support for FTP uploads --- .env.example | 10 +++- package-lock.json | 44 ++++++++++++++++++ package.json | 2 + src/collections/Videos/Videos.ts | 13 ++++++ src/constants.ts | 1 + src/payload.config.ts | 30 +++++++++++- src/plugins/ftpAdapter.ts | 79 ++++++++++++++++++++++++++++++++ src/plugins/mock.js | 1 + src/types/collections.ts | 16 +++++++ 9 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 src/collections/Videos/Videos.ts create mode 100644 src/plugins/ftpAdapter.ts create mode 100644 src/plugins/mock.js diff --git a/.env.example b/.env.example index f9ac36d..6b8a298 100644 --- a/.env.example +++ b/.env.example @@ -11,4 +11,12 @@ STRAPI_TOKEN=strapisecreta5e6ea45ef4e66eaa151612bdcb599df SEEDING_ADMIN_USERNAME=admin_name SEEDING_ADMIN_EMAIL=email@domain.com -SEEDING_ADMIN_PASSWORD=somepassword \ No newline at end of file +SEEDING_ADMIN_PASSWORD=somepassword + +WEB_HOOK_TOKEN=webhooktoken5e6ea45ef4e66eaa151612bdcb599df +WEB_HOOK_URI=https://accords-library.com/some/path + +FTP_USER=someuser +FTP_PASSWORD=somepassword +FTP_HOST=ftp.host.com +FTP_BASE_URL=https://ftp-base-url.com \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 80b77c4..e00085e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,9 @@ "@fontsource/vollkorn": "5.0.19", "@payloadcms/bundler-webpack": "1.0.6", "@payloadcms/db-mongodb": "1.4.4", + "@payloadcms/plugin-cloud-storage": "^1.1.2", "@payloadcms/richtext-lexical": "0.8.0", + "basic-ftp": "^5.0.5", "cross-env": "7.0.3", "language-tags": "1.0.9", "luxon": "3.4.4", @@ -3109,6 +3111,40 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@payloadcms/plugin-cloud-storage": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@payloadcms/plugin-cloud-storage/-/plugin-cloud-storage-1.1.2.tgz", + "integrity": "sha512-wBpEWv4VdfltBqEi5ECSrKQ/TtNvmBWT4DLCrTCOhpNuD8dYD2rp+0Eoj7cF8f6wU7DpS5rvdQ/8gxlqh0Armw==", + "dependencies": { + "find-node-modules": "^2.1.3", + "range-parser": "^1.2.1" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.142.0", + "@aws-sdk/lib-storage": "^3.267.0", + "@azure/abort-controller": "^1.0.0", + "@azure/storage-blob": "^12.11.0", + "@google-cloud/storage": "^6.4.1", + "payload": "^1.7.2 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-s3": { + "optional": true + }, + "@aws-sdk/lib-storage": { + "optional": true + }, + "@azure/abort-controller": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@google-cloud/storage": { + "optional": true + } + } + }, "node_modules/@payloadcms/richtext-lexical": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@payloadcms/richtext-lexical/-/richtext-lexical-0.8.0.tgz", @@ -5116,6 +5152,14 @@ } ] }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", diff --git a/package.json b/package.json index 381c025..c1e681c 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "@fontsource/vollkorn": "5.0.19", "@payloadcms/bundler-webpack": "1.0.6", "@payloadcms/db-mongodb": "1.4.4", + "@payloadcms/plugin-cloud-storage": "^1.1.2", "@payloadcms/richtext-lexical": "0.8.0", + "basic-ftp": "^5.0.5", "cross-env": "7.0.3", "language-tags": "1.0.9", "luxon": "3.4.4", diff --git a/src/collections/Videos/Videos.ts b/src/collections/Videos/Videos.ts new file mode 100644 index 0000000..685b441 --- /dev/null +++ b/src/collections/Videos/Videos.ts @@ -0,0 +1,13 @@ +import { CollectionGroups, Collections } from "../../constants"; +import { buildCollectionConfig } from "../../utils/collectionConfig"; + +export const Videos = buildCollectionConfig({ + slug: Collections.Videos, + labels: { singular: "Video", plural: "Videos" }, + admin: { group: CollectionGroups.Media }, + upload: { + mimeTypes: ["video/*"], + disableLocalStorage: true, + }, + fields: [], +}); diff --git a/src/constants.ts b/src/constants.ts index 2a97703..d856ad5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -19,6 +19,7 @@ export enum Collections { Collectibles = "collectibles", GenericContents = "generic-contents", HomeFolders = "home-folders", + Videos = "videos", } export enum CollectionGroups { diff --git a/src/payload.config.ts b/src/payload.config.ts index 7555174..9ad591a 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -1,5 +1,6 @@ import { webpackBundler } from "@payloadcms/bundler-webpack"; import { mongooseAdapter } from "@payloadcms/db-mongodb"; +import { cloudStorage } from "@payloadcms/plugin-cloud-storage"; import path from "path"; import { buildConfig } from "payload/config"; import { ChronologyEvents } from "./collections/ChronologyEvents/ChronologyEvents"; @@ -15,12 +16,21 @@ import { Pages } from "./collections/Pages/Pages"; import { Recorders } from "./collections/Recorders/Recorders"; import { Tags } from "./collections/Tags/Tags"; import { TagsGroups } from "./collections/TagsGroups/TagsGroups"; +import { Videos } from "./collections/Videos/Videos"; import { Wordings } from "./collections/Wordings/Wordings"; import { Icon } from "./components/Icon"; import { Logo } from "./components/Logo"; import { Collections } from "./constants"; +import { ftpAdapter } from "./plugins/ftpAdapter"; import { createEditor } from "./utils/editor"; +if (!process.env.PAYLOAD_URI) throw new Error("Missing PAYLOAD_URI in .env"); +if (!process.env.MONGODB_URI) throw new Error("Missing MONGODB_URI in .env"); +if (!process.env.FTP_HOST) throw new Error("Missing FTP_HOST in .env"); +if (!process.env.FTP_USER) throw new Error("Missing FTP_USER in .env"); +if (!process.env.FTP_PASSWORD) throw new Error("Missing FTP_PASSWORD in .env"); +if (!process.env.FTP_BASE_URL) throw new Error("Missing FTP_BASE_URL in .env"); + export default buildConfig({ serverURL: process.env.PAYLOAD_URI, admin: { @@ -43,6 +53,7 @@ export default buildConfig({ Notes, Images, + Videos, Tags, TagsGroups, @@ -53,7 +64,7 @@ export default buildConfig({ GenericContents, ], db: mongooseAdapter({ - url: process.env.MONGODB_URI ?? "mongodb://mongo:27017/payload", + url: process.env.MONGODB_URI, }), globals: [HomeFolders], telemetry: false, @@ -63,4 +74,21 @@ export default buildConfig({ graphQL: { disable: true, }, + plugins: [ + cloudStorage({ + collections: { + [Collections.Videos]: { + adapter: ftpAdapter({ + host: process.env.FTP_HOST, + user: process.env.FTP_USER, + password: process.env.FTP_PASSWORD, + secure: false, + endpoint: process.env.FTP_BASE_URL, + }), + disableLocalStorage: true, + disablePayloadAccessControl: true, + }, + }, + }), + ], }); diff --git a/src/plugins/ftpAdapter.ts b/src/plugins/ftpAdapter.ts new file mode 100644 index 0000000..e55add8 --- /dev/null +++ b/src/plugins/ftpAdapter.ts @@ -0,0 +1,79 @@ +import { + Adapter, + GenerateURL, + HandleDelete, + HandleUpload, +} from "@payloadcms/plugin-cloud-storage/dist/types"; +import { Client } from "basic-ftp"; +import path from "path"; +import { Readable } from "stream"; +import type { Configuration as WebpackConfig } from "webpack"; + +interface FTPAdapterConfig { + host: string; + user: string; + password: string; + secure: boolean; + endpoint: string; +} + +export const ftpAdapter = + ({ endpoint, host, password, secure, user }: FTPAdapterConfig): Adapter => + ({ collection }) => { + const generateURL: GenerateURL = ({ filename }) => `${endpoint}/${collection.slug}/${filename}`; + const handleDelete: HandleDelete = async ({ filename }) => { + const client = new Client(); + client.ftp.verbose = true; + await client.access({ + host, + user, + password, + secure, + }); + await client.ensureDir(collection.slug); + await client.remove(filename); + client.close(); + }; + const handleUpload: HandleUpload = async ({ file }) => { + const client = new Client(); + client.ftp.verbose = true; + await client.access({ + host: process.env.FTP_HOST, + user: process.env.FTP_USER, + password: process.env.FTP_PASSWORD, + secure: false, + }); + await client.ensureDir(collection.slug); + await client.uploadFrom(Readable.from(file.buffer), file.filename); + client.close(); + }; + + const webpack = (existingWebpackConfig: WebpackConfig): WebpackConfig => { + const newConfig: WebpackConfig = { + ...existingWebpackConfig, + resolve: { + ...(existingWebpackConfig.resolve || {}), + alias: { + ...(existingWebpackConfig.resolve?.alias ? existingWebpackConfig.resolve.alias : {}), + "./plugins/ftpAdapter": path.resolve(__dirname, "./mock.js"), + }, + fallback: { + ...(existingWebpackConfig.resolve?.fallback + ? existingWebpackConfig.resolve.fallback + : {}), + stream: false, + }, + }, + }; + + return newConfig; + }; + + return { + generateURL, + handleDelete, + handleUpload, + staticHandler: () => {}, + webpack, + }; + }; diff --git a/src/plugins/mock.js b/src/plugins/mock.js new file mode 100644 index 0000000..d35cc76 --- /dev/null +++ b/src/plugins/mock.js @@ -0,0 +1 @@ +export const ftpAdapter = () => {}; diff --git a/src/types/collections.ts b/src/types/collections.ts index 0f4f61c..2f38cfc 100644 --- a/src/types/collections.ts +++ b/src/types/collections.ts @@ -49,6 +49,7 @@ export interface Config { "chronology-events": ChronologyEvent; notes: Note; images: Image; + videos: Video; tags: Tag; "tags-groups": TagsGroup; recorders: Recorder; @@ -622,6 +623,21 @@ export interface Note { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "videos". + */ +export interface Video { + id: string; + updatedAt: string; + createdAt: string; + url?: string | null; + filename?: string | null; + mimeType?: string | null; + filesize?: number | null; + width?: number | null; + height?: number | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "wordings".