UNPKG

aemsync

Version:

The code and content synchronization for Sling / AEM (Adobe Experience Manager).

234 lines (198 loc) 6.63 kB
import fs from 'fs' import globrex from 'globrex' import path from 'path' import * as url from 'url' import util from 'util' import * as log from './log.js' import Zip from './zip.js' const DIRNAME = url.fileURLToPath(new URL('..', import.meta.url)) const DATA_PATH = path.resolve(DIRNAME, 'data') const PACKAGE_CONTENT_PATH = path.join(DATA_PATH, 'package-content') const NT_FOLDER_PATH = path.join(DATA_PATH, 'nt-folder', '.content.xml') const FILTER_ZIP_PATH = 'META-INF/vault/filter.xml' const FILTER_WRAPPER = `<?xml version="1.0" encoding="UTF-8"?> <workspaceFilter version="1.0">%s </workspaceFilter>` const FILTER = ` <filter root="%s" />` const FILTER_CHILDREN = ` <filter root="%s"> <exclude pattern="%s/.*" /> <include pattern="%s" /> <include pattern="%s/.*" /> </filter>` // https://jackrabbit.apache.org/filevault/vaultfs.html export default class Package { constructor (exclude) { this.zip = new Zip() this.exclude = exclude || [] this.entries = [] } // // Path processing. // add (localPath) { // Clean path. localPath = this._cleanPath(localPath) // Added path must be inside 'jcr_root' folder. if (!localPath.includes('jcr_root/')) { return null } // If the change is to an xml file, the parent folder will be processed. // It is better to leave the xml file handling to package manager. if (localPath.endsWith('.xml')) { return this.add(path.dirname(localPath)) } // Include path. const entry = this._deduplicateAndAdd(localPath) if (!entry) { return null } // If folder, add missing .content.xml@nt:folder inside. // This ensures proper handling when removing inner .content.xml file. this._addContentXml(localPath) // Walk up the tree and add all .content.xml files. for (let parentPath = path.dirname(localPath); !parentPath.endsWith('jcr_root'); parentPath = path.dirname(parentPath)) { this._addContentXml(parentPath) } return entry } _addContentXml (localPath) { try { if (fs.lstatSync(localPath).isDirectory()) { const contentXmlPath = path.join(localPath, '.content.xml') if (fs.existsSync(contentXmlPath)) { // Include existing .content.xml. this._deduplicateAndAdd(contentXmlPath) } else { // Include missing .content.xml@nt:folder. // This is needed in case the .content.xml was removed locally. this._deduplicateAndAdd(contentXmlPath, NT_FOLDER_PATH) } } } catch (err) { log.debug(err) } } _deduplicateAndAdd (virtualLocalPath, localPath) { virtualLocalPath = this._cleanPath(virtualLocalPath) // Handle exclusions. if (this._isExcluded(virtualLocalPath)) { return null } // Deduplication handling. const zipPath = this._getZipPath(virtualLocalPath) for (let i = this.entries.length - 1; i >= 0; --i) { const existingZipPath = this.entries[i].zipPath // Skip if already added. if (zipPath === existingZipPath) { return log.debug(`Already added to package, skipping: ${zipPath}`) } // Skip if parent already added (with exception of .content.xml). if (zipPath.startsWith(existingZipPath) && !zipPath.endsWith('.content.xml')) { return log.debug(`Parent already added to package, skipping: ${zipPath}`) } // Remove child if path to add is a parent. if (existingZipPath.startsWith(zipPath)) { log.debug(`Removing child: ${existingZipPath}`) this.entries.splice(i, 1) } } localPath = localPath ? this._cleanPath(localPath) : virtualLocalPath const entry = this._getEntry(localPath, zipPath) this.entries.push(entry) return entry } _isExcluded (localPath) { for (const globPattern of this.exclude) { const regex = globrex(globPattern, { globstar: true, extended: true }).regex if (regex.test(localPath)) { return true } } return false } // // Zip creation. // save (archivePath) { if (this.entries.length === 0) { return {} } try { // Create archive and add default package content. const jcrRoot = path.join(PACKAGE_CONTENT_PATH, 'jcr_root') const metaInf = path.join(PACKAGE_CONTENT_PATH, 'META-INF') this.zip.add(jcrRoot, 'jcr_root') this.zip.add(metaInf, 'META-INF') // Add each entry. const filters = [] for (const entry of this.entries) { if (!entry.exists) { // DELETE // Only filters need to be updated. filters.push(util.format(FILTER, entry.filterPath)) } else { // ADD // Filters need to be updated. const dirName = path.dirname(entry.filterPath) filters.push(util.format(FILTER_CHILDREN, dirName, dirName, entry.filterPath, entry.filterPath)) // ADD // File or folder needs to be added to the zip. this.zip.add(entry.localPath, entry.zipPath) } } // Add filter file. const filter = util.format(FILTER_WRAPPER, filters.join('\n')) this.zip.add(Buffer.from(filter), FILTER_ZIP_PATH) return { path: this.zip.save(archivePath), contents: this.zip.inspect() } } catch (err) { // Can happen in case files change in the meanwhile. return { err } } } // // Entry handling. // // Entry format: // { // localPath: Path to the local file // zipPath: Path inside zip // filterPath: Vault filter path // isFolder // exists // } _getEntry (localPath, zipPath) { localPath = this._cleanPath(localPath) const entry = { localPath, zipPath, filterPath: this._getFilterPath(zipPath) } try { const stat = fs.statSync(localPath) entry.exists = true entry.isFolder = stat.isDirectory() } catch (err) { entry.exists = false } return entry } _cleanPath (localPath) { return path.resolve(localPath) .replace(/\\/g, '/') // Replace backlashes with slashes. .replace(/\/$/, '') // Remove trailing slash. } _getZipPath (localPath) { return this._cleanPath(localPath) .replace(/.*\/(jcr_root\/.*)/, '$1') } _getFilterPath (localPath) { // .content.xml will result in .content entries. // Although incorrect, it does not matter and makes the handling // consistent. return this._cleanPath(localPath) .replace(/(.*jcr_root)|(\.xml$)|(\.dir)/g, '') .replace(/\/_([^/^_]*)_([^/]*)$/g, '/$1:$2') } }