UNPKG

6.67 kBJavaScriptView Raw
1'use strict'
2
3const util = require('util')
4const fs = require('fs')
5const path = require('path')
6const globrex = require('globrex')
7const log = require('./log')
8const defaults = require('./defaults')
9const Zip = require('./zip')
10
11const DATA_PATH = path.resolve(__dirname, '..', 'data')
12const PACKAGE_CONTENT_PATH = path.join(DATA_PATH, 'package-content')
13const NT_FOLDER_PATH = path.join(DATA_PATH, 'nt-folder', '.content.xml')
14const FILTER_ZIP_PATH = 'META-INF/vault/filter.xml'
15const FILTER_WRAPPER = `<?xml version="1.0" encoding="UTF-8"?>
16<workspaceFilter version="1.0">%s
17</workspaceFilter>`
18const FILTER = `
19 <filter root="%s" />`
20const FILTER_CHILDREN = `
21 <filter root="%s">
22 <exclude pattern="%s/.*" />
23 <include pattern="%s" />
24 <include pattern="%s/.*" />
25 </filter>`
26
27// https://jackrabbit.apache.org/filevault/vaultfs.html
28class Package {
29 constructor (exclude = defaults.exclude) {
30 this.zip = new Zip()
31 this.exclude = exclude || []
32 this.entries = []
33 }
34
35 //
36 // Path processing.
37 //
38
39 add (localPath) {
40 // Clean path.
41 localPath = this._cleanPath(localPath)
42
43 // Added path must be inside 'jcr_root' folder.
44 if (!localPath.includes('jcr_root/')) {
45 return null
46 }
47
48 // If the change is to an xml file, the parent folder will be processed.
49 // It is better to leave the xml file handling to package manager.
50 if (localPath.endsWith('.xml')) {
51 return this.add(path.dirname(localPath))
52 }
53
54 // Include path.
55 const entry = this._deduplicateAndAdd(localPath)
56 if (!entry) {
57 return null
58 }
59
60 // If folder, Add missing .content.xml@nt:folder inside.
61 // This ensures proper handlig when removing inner .content.xml file.
62 this._addContentXml(localPath)
63
64 // Walk up the tree and add all .content.xml files.
65 for (let parentPath = path.dirname(localPath); !parentPath.endsWith('jcr_root'); parentPath = path.dirname(parentPath)) {
66 this._addContentXml(parentPath)
67 }
68
69 return entry
70 }
71
72 _addContentXml (localPath) {
73 try {
74 if (fs.lstatSync(localPath).isDirectory()) {
75 const contentXmlPath = path.join(localPath, '.content.xml')
76 if (fs.existsSync(contentXmlPath)) {
77 // Include existing .content.xml.
78 this._deduplicateAndAdd(contentXmlPath)
79 } else {
80 // Include missing .content.xml@nt:folder.
81 // This is needed in case the .content.xml was removed locally.
82 this._deduplicateAndAdd(contentXmlPath, NT_FOLDER_PATH)
83 }
84 }
85 } catch (err) {
86 log.debug(err)
87 }
88 }
89
90 _deduplicateAndAdd (virtualLocalPath, localPath) {
91 virtualLocalPath = this._cleanPath(virtualLocalPath)
92
93 // Handle exclusions.
94 if (this._isExcluded(virtualLocalPath)) {
95 return null
96 }
97
98 // Deduplication handling.
99 const zipPath = this._getZipPath(virtualLocalPath)
100 for (let i = this.entries.length - 1; i >= 0; --i) {
101 const existingZipPath = this.entries[i].zipPath
102
103 // Skip if already added.
104 if (zipPath === existingZipPath) {
105 return log.debug(`Already added to package, skipping: ${zipPath}`)
106 }
107
108 // Skip if parent already added (with exception of .content.xml).
109 if (zipPath.startsWith(existingZipPath) && !zipPath.endsWith('.content.xml')) {
110 return log.debug(`Parent already added to package, skipping: ${zipPath}`)
111 }
112
113 // Remove child if path to add is a parent.
114 if (existingZipPath.startsWith(zipPath)) {
115 log.debug(`Removing child: ${existingZipPath}`)
116 this.entries.splice(i, 1)
117 }
118 }
119
120 localPath = localPath ? this._cleanPath(localPath) : virtualLocalPath
121 const entry = this._getEntry(localPath, zipPath)
122 this.entries.push(entry)
123 return entry
124 }
125
126 _isExcluded (localPath) {
127 for (const globPattern of this.exclude) {
128 const regex = globrex(globPattern, { globstar: true, extended: true }).regex
129 if (regex.test(localPath)) {
130 return true
131 }
132 }
133
134 return false
135 }
136
137 //
138 // Zip creation.
139 //
140
141 save (archivePath) {
142 if (this.entries.length === 0) {
143 return null
144 }
145
146 // Create archive and add default package content.
147 const jcrRoot = path.join(PACKAGE_CONTENT_PATH, 'jcr_root')
148 const metaInf = path.join(PACKAGE_CONTENT_PATH, 'META-INF')
149 this.zip.add(jcrRoot, 'jcr_root')
150 this.zip.add(metaInf, 'META-INF')
151
152 // Add each entry.
153 const filters = []
154 for (const entry of this.entries) {
155 if (!entry.exists) {
156 // DELETE
157 // Only filters need to be updated.
158 filters.push(util.format(FILTER, entry.filterPath))
159 } else {
160 // ADD
161 // Filters need to be updated.
162 const dirName = path.dirname(entry.filterPath)
163 // if (!entry.localPath.endsWith('.content.xml')) {
164 filters.push(util.format(FILTER_CHILDREN, dirName, dirName, entry.filterPath, entry.filterPath))
165 // }
166
167 // ADD
168 // File or folder needs to be added to the zip.
169 this.zip.add(entry.localPath, entry.zipPath)
170 }
171 }
172
173 // Add filter file.
174 const filter = util.format(FILTER_WRAPPER, filters.join('\n'))
175 this.zip.add(Buffer.from(filter), FILTER_ZIP_PATH)
176
177 // Debug package contents.
178 log.debug('Package details:')
179 log.group()
180 log.debug(JSON.stringify(this.zip.inspect(), null, 2))
181 log.groupEnd()
182
183 return this.zip.save(archivePath)
184 }
185
186 //
187 // Entry handling.
188 //
189
190 // Entry format:
191 // {
192 // localPath: Path to the local file
193 // zipPath: Path inside zip
194 // filterPath: Vault filter path
195 // isFolder
196 // exists
197 // }
198 _getEntry (localPath, zipPath) {
199 localPath = this._cleanPath(localPath)
200
201 const entry = {
202 localPath,
203 zipPath,
204 filterPath: this._getFilterPath(zipPath)
205 }
206
207 try {
208 const stat = fs.statSync(localPath)
209 entry.exists = true
210 entry.isFolder = stat.isDirectory()
211 } catch (err) {
212 entry.exists = false
213 }
214
215 return entry
216 }
217
218 _cleanPath (localPath) {
219 return path.resolve(localPath)
220 .replace(/\\/g, '/') // Replace backlashes with slashes.
221 .replace(/\/$/, '') // Remove trailing slash.
222 }
223
224 _getZipPath (localPath) {
225 return this._cleanPath(localPath)
226 .replace(/.*\/(jcr_root\/.*)/, '$1')
227 }
228
229 _getFilterPath (localPath) {
230 // .content.xml will result in .content entries.
231 // Although incorrect, it does not matter and makes the handling
232 // consistent.
233 return this._cleanPath(localPath)
234 .replace(/(.*jcr_root)|(\.xml$)|(\.dir)/g, '')
235 .replace(/\/_([^/^_]*)_([^/]*)$/g, '/$1:$2')
236 }
237}
238
239module.exports = Package