Skip to content

Commit

Permalink
duplicate website api
Browse files Browse the repository at this point in the history
  • Loading branch information
lexoyo committed Sep 14, 2023
1 parent 2edbc29 commit 29bea27
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 12 deletions.
Binary file added public/assets/favicon.ico
Binary file not shown.
14 changes: 7 additions & 7 deletions src/ts/client/publication-transformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,13 @@ export const publicationTransformerDefault: PublicationTransformer = {
// Define files URLs
transformPermalink(link: string, type: ClientSideFileType, initiator: Initiator): string {
switch(initiator) {
case Initiator.HTML:
return link
case Initiator.CSS:
// In case of a link from a CSS file, we need to go up one level
return `../${link.replace(/^\//, '')}`
default:
throw new Error(`Unknown initiator ${initiator}`)
case Initiator.HTML:
return link
case Initiator.CSS:
// In case of a link from a CSS file, we need to go up one level
return `../${link.replace(/^\//, '')}`
default:
throw new Error(`Unknown initiator ${initiator}`)
}
},
// Define how files are named
Expand Down
1 change: 1 addition & 0 deletions src/ts/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const API_WEBSITE_READ = '/'
export const API_WEBSITE_WRITE = '/'
export const API_WEBSITE_CREATE = '/'
export const API_WEBSITE_DELETE = '/'
export const API_WEBSITE_DUPLICATE = '/duplicate'
export const API_WEBSITE_LIST = '/'
export const API_WEBSITE_ASSET_READ = '/assets'
export const API_WEBSITE_ASSETS_WRITE = '/assets'
Expand Down
15 changes: 13 additions & 2 deletions src/ts/plugins/server/FtpConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ import { WEBSITE_DATA_FILE, WEBSITE_META_DATA_FILE } from '../../constants'
import { ConnectorType, ConnectorUser, WebsiteMeta, FileMeta, JobData, JobStatus, WebsiteId, PublicationJobData, WebsiteMetaFileContent, defaultWebsiteData, WebsiteData, ConnectorOptions } from '../../types'
import { ServerConfig } from '../../server/config'
import { join } from 'path'
import { type } from 'os'
import { tmpdir } from 'os'
import { v4 as uuid } from 'uuid'
import { JobManager } from '../../server/jobs'
import { mkdtemp } from 'fs/promises'

/**
* @fileoverview FTP connector for Silex
Expand Down Expand Up @@ -338,7 +339,6 @@ export default class FtpConnector implements StorageConnector<FtpSession> {

async getLoginForm(session: FtpSession, redirectTo: string): Promise<string | null> {
const { host, user, pass, port, secure, publicationPath, storageRootPath, websiteUrl } = this.sessionData(session)
requiredParam(type, 'connector type')
return `
<style>
${formCss}
Expand Down Expand Up @@ -491,6 +491,17 @@ export default class FtpConnector implements StorageConnector<FtpSession> {
this.closeClient(ftp)
}

async duplicateWebsite(session: FtpSession, websiteId: string, newWebsiteId: string): Promise<void> {
const storageRootPath = this.rootPath(session)
const ftp = await this.getClient(this.sessionData(session))
const websitePath = join(storageRootPath, websiteId)
const newWebsitePath = join(storageRootPath, newWebsiteId)
const tempDir = await mkdtemp(tmpdir())
await ftp.downloadToDir(websitePath, tempDir)
await ftp.uploadFromDir(tempDir, newWebsitePath)
this.closeClient(ftp)
}

async writeAssets(
session: any,
id: WebsiteId,
Expand Down
33 changes: 31 additions & 2 deletions src/ts/server/api/websiteApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
import { Router } from 'express'
import formidable from 'formidable'
import PersistentFile from 'formidable/src/PersistentFile'
import { API_WEBSITE_ASSET_READ, API_WEBSITE_ASSETS_WRITE, API_WEBSITE_READ, API_WEBSITE_WRITE, API_WEBSITE_DELETE, API_WEBSITE_META_READ, API_WEBSITE_META_WRITE, API_WEBSITE_LIST, API_WEBSITE_CREATE, API_PATH, API_WEBSITE_PATH } from '../../constants'
import { API_WEBSITE_ASSET_READ, API_WEBSITE_ASSETS_WRITE, API_WEBSITE_READ, API_WEBSITE_WRITE, API_WEBSITE_DELETE, API_WEBSITE_META_READ, API_WEBSITE_META_WRITE, API_WEBSITE_LIST, API_WEBSITE_CREATE, API_PATH, API_WEBSITE_PATH, API_WEBSITE_DUPLICATE } from '../../constants'
import { createReadStream } from 'fs'
import { ApiError, ApiWebsiteAssetsReadParams, ApiWebsiteAssetsReadQuery, ApiWebsiteAssetsReadResponse, ApiWebsiteAssetsWriteQuery, ApiWebsiteAssetsWriteResponse, ApiWebsiteDeleteQuery, ApiWebsiteReadQuery, ApiWebsiteReadResponse, ApiWebsiteWriteBody, ApiWebsiteWriteQuery, ConnectorId, ConnectorType, WebsiteMeta, WebsiteData, WebsiteId, ApiWebsiteListQuery, ApiWebsiteListResponse, ApiWebsiteMetaReadQuery, ApiWebsiteMetaReadResponse, ApiWebsiteMetaWriteQuery, ApiWebsiteMetaWriteBody, WebsiteMetaFileContent, ApiWebsiteMetaWriteResponse, ApiWebsiteWriteResponse, ApiWebsiteCreateQuery, ApiWebsiteCreateBody } from '../../types'
import { ApiError, ApiWebsiteAssetsReadParams, ApiWebsiteAssetsReadQuery, ApiWebsiteAssetsReadResponse, ApiWebsiteAssetsWriteQuery, ApiWebsiteAssetsWriteResponse, ApiWebsiteDeleteQuery, ApiWebsiteReadQuery, ApiWebsiteReadResponse, ApiWebsiteWriteBody, ApiWebsiteWriteQuery, ConnectorId, ConnectorType, WebsiteMeta, WebsiteData, WebsiteId, ApiWebsiteListQuery, ApiWebsiteListResponse, ApiWebsiteMetaReadQuery, ApiWebsiteMetaReadResponse, ApiWebsiteMetaWriteQuery, ApiWebsiteMetaWriteBody, WebsiteMetaFileContent, ApiWebsiteMetaWriteResponse, ApiWebsiteWriteResponse, ApiWebsiteCreateQuery, ApiWebsiteCreateBody, ApiWebsiteDuplicateQuery } from '../../types'
import { ConnectorFile, ConnectorFileContent, ConnectorSession, StorageConnector, getConnector } from '../connectors/connectors'
import { Readable } from 'stream'
import { requiredParam } from '../utils/validation'
Expand Down Expand Up @@ -226,6 +226,24 @@ export default function (config: ServerConfig, opts = {}): Router {
}
})

// Duplicate website
router.post(API_WEBSITE_DUPLICATE, async (req, res) => {
try {
const query: ApiWebsiteDuplicateQuery = req.query as any
const fromId= requiredParam<WebsiteId>(query.fromId, 'Website id')
const toId= requiredParam<WebsiteId>(query.toId, 'New website id')
await duplicateWebsite(req['session'], fromId, toId, query.connectorId)
res.status(200).json({ message: 'Website duplicated' } as ApiError)
} catch (e) {
console.error('Error duplicating website data', e)
if (e.httpStatusCode) {
res.status(e.httpStatusCode).json({ message: e.message } as ApiError)
} else {
res.status(500).json({ message: e.message } as ApiError)
}
}
})

// Load assets
router.get(API_WEBSITE_ASSET_READ + '/:path', async (req, res) => {
{
Expand Down Expand Up @@ -391,6 +409,17 @@ export default function (config: ServerConfig, opts = {}): Router {
return storageConnector.deleteWebsite(session, websiteId)
}

/**
* Duplicate a website
*/
async function duplicateWebsite(session: any, websiteId: string, newWebsiteId: string, connectorId?: string): Promise<void> {
// Get the desired connector
const storageConnector = await getStorageConnector(session, connectorId)

// Duplicate the website
return storageConnector.duplicateWebsite(session, websiteId, newWebsiteId)
}

/**
* Read an asset
*/
Expand Down
22 changes: 22 additions & 0 deletions src/ts/server/connectors/FsStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@ if(!globalThis.__dirname) {
console.info('Redefining __dirname', globalThis.__dirname)
}

// Copy a folder recursively
async function copyDir(src, dest) {
await fs.mkdir(dest, { recursive: true })
const entries = await fs.readdir(src, { withFileTypes: true })

for (const entry of entries) {
const srcPath = join(src, entry.name)
const destPath = join(dest, entry.name)

entry.isDirectory() ?
await copyDir(srcPath, destPath) :
await fs.copyFile(srcPath, destPath)
}
}


type FsSession = ConnectorSession

const USER_ICON = 'data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' height=\'1em\' viewBox=\'0 0 448 512\'%3E%3Cpath d=\'M304 128a80 80 0 1 0 -160 0 80 80 0 1 0 160 0zM96 128a128 128 0 1 1 256 0A128 128 0 1 1 96 128zM49.3 464H398.7c-8.9-63.3-63.3-112-129-112H178.3c-65.7 0-120.1 48.7-129 112zM0 482.3C0 383.8 79.8 304 178.3 304h91.4C368.2 304 448 383.8 448 482.3c0 16.4-13.3 29.7-29.7 29.7H29.7C13.3 512 0 498.7 0 482.3z\'/%3E%3C/svg%3E'
Expand Down Expand Up @@ -182,6 +198,12 @@ export class FsStorage implements StorageConnector<FsSession> {
return fs.rmdir(path, { recursive: true })
}

async duplicateWebsite(session: FsSession, websiteId: WebsiteId, newWebsiteId: WebsiteId): Promise<void> {
const from = join(this.options.path, websiteId)
const to = join(this.options.path, newWebsiteId)
await copyDir(from, to)
}

async listWebsites(session: any): Promise<WebsiteMeta[]> {
const list = await fs.readdir(this.options.path)
return Promise.all(list.map(async fileName => {
Expand Down
2 changes: 1 addition & 1 deletion src/ts/server/connectors/connectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export interface StorageConnector<Session extends ConnectorSession = ConnectorSe
createWebsite(session: Session, data: WebsiteMetaFileContent): Promise<WebsiteId>
updateWebsite(session: Session, websiteId: WebsiteId, data: WebsiteData): Promise<void>
deleteWebsite(session: Session, websiteId: WebsiteId): Promise<void>
duplicateWebsite(session: Session, websiteId: WebsiteId, newWebsiteId: WebsiteId): Promise<void>
// CRUD on assets
writeAssets(session: Session, websiteId: WebsiteId, files: ConnectorFile[], status?: StatusCallback): Promise<string[] | void>
readAsset(session: Session, websiteId: WebsiteId, fileName: string): Promise<ConnectorFileContent>
Expand All @@ -102,7 +103,6 @@ export interface StorageConnector<Session extends ConnectorSession = ConnectorSe
export interface HostingConnector<Session extends ConnectorSession = ConnectorSession> extends Connector<Session> {
publish(session: Session, websiteId: WebsiteId, files: ConnectorFile[], jobManager: JobManager): Promise<JobData> // Pass the jobManager as plugins do not neccessarily share the same module instance
getUrl(session: Session, websiteId: WebsiteId): Promise<string>

}

export function toConnectorEnum(type: string | ConnectorType): ConnectorType {
Expand Down
1 change: 1 addition & 0 deletions src/ts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export type ApiWebsiteCreateQuery = { connectorId?: ConnectorId }
export type ApiWebsiteCreateBody = WebsiteMetaFileContent
export type ApiWebsiteCreateResponse = { message: string }
export type ApiWebsiteDeleteQuery = { websiteId: WebsiteId, connectorId?: ConnectorId }
export type ApiWebsiteDuplicateQuery = { fromId: WebsiteId, toId: WebsiteId, connectorId?: ConnectorId }
export type ApiWebsiteMetaReadQuery = { websiteId: WebsiteId, connectorId?: ConnectorId }
export type ApiWebsiteMetaReadResponse = WebsiteMeta
export type ApiWebsiteMetaWriteQuery = { websiteId: WebsiteId, connectorId?: ConnectorId }
Expand Down

0 comments on commit 29bea27

Please sign in to comment.