| 
									
										
										
										
											2022-08-03 01:35:19 +03:00
										 |  |  | /********************************************************************** | 
					
						
							|  |  |  | *  | 
					
						
							|  |  |  | * | 
					
						
							|  |  |  | * | 
					
						
							|  |  |  | **********************************************************************/ | 
					
						
							|  |  |  | ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) | 
					
						
							|  |  |  | (function(require){ var module={} // make module AMD/node compatible...
 | 
					
						
							|  |  |  | /*********************************************************************/ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var fs = require('fs') | 
					
						
							|  |  |  | var glob = require('glob') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var object = require('ig-object') | 
					
						
							|  |  |  | var types = require('ig-types') | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-05 18:21:18 +03:00
										 |  |  | var pwpath = require('../path') | 
					
						
							| 
									
										
										
										
											2022-08-03 01:35:19 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-08-05 18:21:18 +03:00
										 |  |  | var base = require('./base') | 
					
						
							| 
									
										
										
										
											2022-08-03 01:35:19 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | //---------------------------------------------------------------------
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // XXX structure is not final...
 | 
					
						
							|  |  |  | // 		- need to split each adapter into modules...
 | 
					
						
							|  |  |  | // 		- should the media handler api be merged with store???
 | 
					
						
							|  |  |  | // 		- how do we handle config???
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var FILESTORE_OPTIONS = { | 
					
						
							|  |  |  | 	index: '.index', | 
					
						
							|  |  |  | 	backup: '/.backup', | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	clearEmptyDir: true, | 
					
						
							|  |  |  | 	dirToFile: true, | 
					
						
							|  |  |  | 	cleanBackup: true, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	verbose: true, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var getOpts =  | 
					
						
							|  |  |  | function(opts){ | 
					
						
							|  |  |  | 	return { | 
					
						
							|  |  |  | 		...FILESTORE_OPTIONS, | 
					
						
							|  |  |  | 		...(opts ?? {}), | 
					
						
							|  |  |  | 	} } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | //	func(base[, options])
 | 
					
						
							|  |  |  | //		-> true/false
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | //	func(base, path[, options])
 | 
					
						
							|  |  |  | //		-> true/false
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // XXX not yet sure how w should handle dot-files....
 | 
					
						
							|  |  |  | // XXX should these be store methods???
 | 
					
						
							|  |  |  | // XXX do we need error checking in these???
 | 
					
						
							|  |  |  | var exists = | 
					
						
							|  |  |  | module.exists = | 
					
						
							|  |  |  | async function(base, sub, options){ | 
					
						
							|  |  |  | 	if(typeof(sub) != 'string'){ | 
					
						
							|  |  |  | 		options = sub ?? options | 
					
						
							|  |  |  | 		sub = base | 
					
						
							|  |  |  | 		base = null } | 
					
						
							|  |  |  | 	var {index} = getOpts(options) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var target = base ? | 
					
						
							|  |  |  | 		pwpath.join(base, sub) | 
					
						
							|  |  |  | 		: sub | 
					
						
							|  |  |  | 	if(!fs.existsSync(target)){ | 
					
						
							|  |  |  | 		return false } | 
					
						
							|  |  |  | 	var stat = await fs.promises.stat(target) | 
					
						
							|  |  |  | 	if(stat.isDirectory()){ | 
					
						
							|  |  |  | 		return fs.existsSync(pwpath.join(target, index)) } | 
					
						
							|  |  |  | 	return true } | 
					
						
							|  |  |  | var read = | 
					
						
							|  |  |  | module.read = | 
					
						
							|  |  |  | async function(base, sub, options){ | 
					
						
							|  |  |  | 	if(typeof(sub) != 'string'){ | 
					
						
							|  |  |  | 		options = sub ?? options | 
					
						
							|  |  |  | 		sub = base | 
					
						
							|  |  |  | 		base = null } | 
					
						
							|  |  |  | 	var {index} = getOpts(options) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var target = base ? | 
					
						
							|  |  |  | 		pwpath.join(base, sub) | 
					
						
							|  |  |  | 		: sub | 
					
						
							|  |  |  | 	if(!fs.existsSync(target)){ | 
					
						
							|  |  |  | 		return undefined } | 
					
						
							|  |  |  | 	// handle dir text...
 | 
					
						
							|  |  |  | 	var stat = await fs.promises.stat(target) | 
					
						
							|  |  |  | 	if(stat.isDirectory()){ | 
					
						
							|  |  |  | 		var target = pwpath.join(target, index)  | 
					
						
							|  |  |  | 		fs.existsSync(target) | 
					
						
							|  |  |  | 			|| (target = false) } | 
					
						
							|  |  |  | 	return target ? | 
					
						
							|  |  |  | 		fs.promises.readFile(target, {encoding: 'utf-8'}) | 
					
						
							|  |  |  | 		: undefined } | 
					
						
							|  |  |  | var mkdir =  | 
					
						
							|  |  |  | module.mkdir = | 
					
						
							|  |  |  | async function(base, sub, options){ | 
					
						
							|  |  |  | 	if(typeof(sub) != 'string'){ | 
					
						
							|  |  |  | 		options = sub ?? options | 
					
						
							|  |  |  | 		sub = base  | 
					
						
							|  |  |  | 		base = null } | 
					
						
							|  |  |  | 	var {index, verbose} = getOpts(options) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var levels = pwpath.split(sub) | 
					
						
							|  |  |  | 	for(var level of levels){ | 
					
						
							|  |  |  | 		base = base == null ? | 
					
						
							|  |  |  | 			level | 
					
						
							|  |  |  | 			: pwpath.join(base, level) | 
					
						
							|  |  |  | 		// nothing exists -- create dir and continue...
 | 
					
						
							|  |  |  | 		if(!fs.existsSync(base)){ | 
					
						
							|  |  |  | 			verbose  | 
					
						
							|  |  |  | 				&& console.log('mkdir(..): mkdir:', base) | 
					
						
							|  |  |  | 			await fs.promises.mkdir(base, {recursive: true})  | 
					
						
							|  |  |  | 			continue } | 
					
						
							|  |  |  | 		// directory -- continue...
 | 
					
						
							|  |  |  | 		var stat = await fs.promises.stat(base) | 
					
						
							|  |  |  | 		if(stat.isDirectory()){ | 
					
						
							|  |  |  | 			continue } | 
					
						
							|  |  |  | 		// file -- convert to dir...
 | 
					
						
							|  |  |  | 		verbose  | 
					
						
							|  |  |  | 			&& console.log('mkdir(..): converting file to dir:', base) | 
					
						
							|  |  |  | 		await fs.promises.rename(base, base+'.pwiki-bak')  | 
					
						
							|  |  |  | 		await fs.promises.mkdir(base, {recursive: true})  | 
					
						
							|  |  |  | 		await fs.promises.rename(base +'.pwiki-bak', base +'/'+ index) } | 
					
						
							|  |  |  | 	return base } | 
					
						
							|  |  |  | // XXX metadata???
 | 
					
						
							|  |  |  | var update =  | 
					
						
							|  |  |  | module.update = | 
					
						
							|  |  |  | async function(base, sub, data, options){ | 
					
						
							|  |  |  | 	if(typeof(data) != 'string'){ | 
					
						
							|  |  |  | 		options = data ?? options | 
					
						
							|  |  |  | 		data = sub | 
					
						
							|  |  |  | 		sub = base | 
					
						
							|  |  |  | 		base = null } | 
					
						
							|  |  |  | 	var {index} = getOpts(options) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	var target = base ? | 
					
						
							|  |  |  | 		pwpath.join(base, sub) | 
					
						
							|  |  |  | 		: sub | 
					
						
							|  |  |  | 	// path already exists...
 | 
					
						
							|  |  |  | 	if(fs.existsSync(target)){ | 
					
						
							|  |  |  | 		var stat = await fs.promises.stat(target) | 
					
						
							|  |  |  | 		if(stat.isDirectory()){ | 
					
						
							|  |  |  | 			target = pwpath.join(target, index) }  | 
					
						
							|  |  |  | 	// create path / parts of path...
 | 
					
						
							|  |  |  | 	} else { | 
					
						
							|  |  |  | 		var levels = pwpath.split(target) | 
					
						
							|  |  |  | 		levels.pop() | 
					
						
							|  |  |  | 		// ensure the parent dir exists...
 | 
					
						
							|  |  |  | 		await module.mkdir( | 
					
						
							|  |  |  | 			...(base ? | 
					
						
							|  |  |  | 				// NOTE: we are keeping this separate here to avoid creating 
 | 
					
						
							|  |  |  | 				// 		anything above it...
 | 
					
						
							|  |  |  | 				[base] | 
					
						
							|  |  |  | 				: []),  | 
					
						
							|  |  |  | 			levels,  | 
					
						
							|  |  |  | 			options) } | 
					
						
							|  |  |  | 	// write the data...
 | 
					
						
							|  |  |  | 	var f = await fs.promises.open(target, 'w') | 
					
						
							|  |  |  | 	await f.writeFile(data) | 
					
						
							|  |  |  | 	f.close() | 
					
						
							|  |  |  | 	return target } | 
					
						
							|  |  |  | var clear =  | 
					
						
							|  |  |  | module.clear = | 
					
						
							|  |  |  | async function(base, sub, options){ | 
					
						
							|  |  |  | 	if(typeof(sub) != 'string'){ | 
					
						
							|  |  |  | 		options = sub ?? options | 
					
						
							|  |  |  | 		sub = base | 
					
						
							|  |  |  | 		base = '' } | 
					
						
							|  |  |  | 	var {index} = getOpts(options) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// remove leaf...
 | 
					
						
							|  |  |  | 	var target = base == '' ? | 
					
						
							|  |  |  | 		sub | 
					
						
							|  |  |  | 		: pwpath.join(base, sub) | 
					
						
							|  |  |  | 	// dir...
 | 
					
						
							|  |  |  | 	if(fs.existsSync(target)){ | 
					
						
							|  |  |  | 		var stat = await fs.promises.stat(target) | 
					
						
							|  |  |  | 		if(stat.isDirectory()){ | 
					
						
							|  |  |  | 			var files = await fs.promises.readdir(target) | 
					
						
							|  |  |  | 			// remove index...
 | 
					
						
							|  |  |  | 			if(files.includes(index)){ | 
					
						
							|  |  |  | 				await fs.promises.rm(pwpath.join(target, index)) | 
					
						
							|  |  |  | 				// NOTE: we do not care what we pop as long as the .length 
 | 
					
						
							|  |  |  | 				// 		is correct as we'll not be using the content after 
 | 
					
						
							|  |  |  | 				// 		this point...
 | 
					
						
							|  |  |  | 				files.pop() } | 
					
						
							|  |  |  | 			// remove dir if empty...
 | 
					
						
							|  |  |  | 			if(files.length == 0){ | 
					
						
							|  |  |  | 				fs.promises.rmdir(target) } | 
					
						
							|  |  |  | 		// simple file...
 | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			await fs.promises.rm(target) } } | 
					
						
							|  |  |  | 	// cleanup path -- remove empty dirs... (XXX ???)
 | 
					
						
							|  |  |  | 	var levels = pwpath.split(sub) | 
					
						
							|  |  |  | 		.slice(0, -1) | 
					
						
							|  |  |  | 	while(levels.length > 0){ | 
					
						
							|  |  |  | 		var cur = pwpath.join(base, ...levels) | 
					
						
							|  |  |  | 		if(fs.existsSync(cur)){ | 
					
						
							|  |  |  | 			var stat = await fs.promises.stat(base) | 
					
						
							|  |  |  | 			if(stat.isDirectory()){ | 
					
						
							|  |  |  | 				// stop cleanup if non-empty dir...
 | 
					
						
							|  |  |  | 				if((await fs.promises.readdir(cur)).length != 0){ | 
					
						
							|  |  |  | 					break } | 
					
						
							|  |  |  | 				fs.promises.rmdir(cur) } } | 
					
						
							|  |  |  | 		levels.pop() }  | 
					
						
							|  |  |  | 	return target } | 
					
						
							|  |  |  | var cleanup = | 
					
						
							|  |  |  | module.cleanup = | 
					
						
							|  |  |  | async function(base, options){ | 
					
						
							|  |  |  | 	var {index, clearEmptyDir, dirToFile, verbose} = getOpts(options) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	glob(pwpath.join(base, '**/*')) | 
					
						
							|  |  |  | 		.on('end', async function(paths){ | 
					
						
							|  |  |  | 			paths | 
					
						
							|  |  |  | 				.sort(function(a, b){ | 
					
						
							|  |  |  | 					return b.length - a.length }) | 
					
						
							|  |  |  | 			for(var path of paths){ | 
					
						
							|  |  |  | 				var stat = await fs.promises.stat(path) | 
					
						
							|  |  |  | 				if(stat.isDirectory()){ | 
					
						
							|  |  |  | 					var children = await fs.promises.readdir(path) | 
					
						
							|  |  |  | 					// empty -> remove...
 | 
					
						
							|  |  |  | 					if(clearEmptyDir  | 
					
						
							|  |  |  | 							&& children.length == 0){ | 
					
						
							|  |  |  | 						verbose  | 
					
						
							|  |  |  | 							&& console.log('cleanup(..): removing dir:', path) | 
					
						
							|  |  |  | 						fs.promises.rmdir(path) | 
					
						
							|  |  |  | 						continue } | 
					
						
							|  |  |  | 					// dir -> file...
 | 
					
						
							|  |  |  | 					if(dirToFile | 
					
						
							|  |  |  | 							&& children.length == 1  | 
					
						
							|  |  |  | 							&& children[0] == index){ | 
					
						
							|  |  |  | 						verbose  | 
					
						
							|  |  |  | 							&& console.log('cleanup(..): converting dir to file:', path) | 
					
						
							|  |  |  | 						await fs.promises.rename(path +'/'+ index, path+'.pwiki-bak')  | 
					
						
							|  |  |  | 						await fs.promises.rmdir(path)  | 
					
						
							|  |  |  | 						await fs.promises.rename(path +'.pwiki-bak', path) | 
					
						
							|  |  |  | 						continue } | 
					
						
							|  |  |  | 				} } }) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // XXX backup metadata...
 | 
					
						
							|  |  |  | // 		- date
 | 
					
						
							|  |  |  | // 		- reason
 | 
					
						
							|  |  |  | // 		- refs...
 | 
					
						
							|  |  |  | // XXX set hidden attribute on backup dir...
 | 
					
						
							|  |  |  | // XXX add backup packing...
 | 
					
						
							|  |  |  | var backup = | 
					
						
							|  |  |  | module.backup = { | 
					
						
							|  |  |  | 	// XXX backup config???
 | 
					
						
							|  |  |  | 	//index: '.index',
 | 
					
						
							|  |  |  | 	//base: '/.backup',
 | 
					
						
							|  |  |  | 	//cleanBackup: true,
 | 
					
						
							|  |  |  | 	//verbose: true,
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// 	.create(<base>[, <options>])
 | 
					
						
							|  |  |  | 	// 	.create(<base>, '**'[, <options>])
 | 
					
						
							|  |  |  | 	// 	.create(<base>, '**', Date.timeStamp()[, <options>])
 | 
					
						
							|  |  |  | 	// 		-> <list>
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// 	.create(<base>, <path>[, <version>][, <options>])
 | 
					
						
							|  |  |  | 	// 		-> <list>
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// 	.create(<base>, <path>, false[, <options>])
 | 
					
						
							|  |  |  | 	// 		-> <list>
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// .create(..) and .restore(..) are completely symmetrical.
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// NOTE: backing up ** will include nested backups but will skip the 
 | 
					
						
							|  |  |  | 	// 		root backup but will ignore the root backup dir...
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// XXX since these are *almost* identical in structure, can we reuse one 
 | 
					
						
							|  |  |  | 	// 		to implement the other???
 | 
					
						
							|  |  |  | 	// 		..or can we implement these in a manner similar to "cp A B" vs. "cp B A"???
 | 
					
						
							|  |  |  | 	create: async function(base, sub='**', version=Date.timeStamp(), options){ | 
					
						
							|  |  |  | 		var that = this | 
					
						
							|  |  |  | 		if(typeof(sub) == 'object'){ | 
					
						
							|  |  |  | 			options = sub | 
					
						
							|  |  |  | 			sub = '**' } | 
					
						
							|  |  |  | 		if(typeof(version) == 'object'){ | 
					
						
							|  |  |  | 			options = version | 
					
						
							|  |  |  | 			version = Date.timeStamp() } | 
					
						
							|  |  |  | 		// options...
 | 
					
						
							|  |  |  | 		var {index, backup, verbose, recursive, cleanBackup, __batch} = options = getOpts(options) | 
					
						
							|  |  |  | 		recursive = recursive ?? false | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		var _backup = backup =  | 
					
						
							|  |  |  | 			version ? | 
					
						
							|  |  |  | 				pwpath.join(backup, version) | 
					
						
							|  |  |  | 				: backup | 
					
						
							|  |  |  | 		backup =  | 
					
						
							|  |  |  | 			pwpath.join( | 
					
						
							|  |  |  | 				base, | 
					
						
							|  |  |  | 				pwpath.relative(pwpath.dirname(sub), backup)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// ** or * -- backup each file in path...
 | 
					
						
							|  |  |  | 		if(/[\\\/]*\*\*?$/.test(sub)){ | 
					
						
							|  |  |  | 			if(sub.endsWith('**')){ | 
					
						
							|  |  |  | 				options.recursive = true } | 
					
						
							|  |  |  | 			options.__batch = true | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if(cleanBackup  | 
					
						
							|  |  |  | 					&& fs.existsSync(backup)){ | 
					
						
							|  |  |  | 				verbose | 
					
						
							|  |  |  | 					&& console.log('.create(..): cleaning:', backup) | 
					
						
							|  |  |  | 				await fs.promises.rm(backup, {recursive: true}) } | 
					
						
							|  |  |  | 			sub = sub.replace(/[\\\/]*\*\*?$/, '') | 
					
						
							|  |  |  | 			var b = pwpath.split(_backup) | 
					
						
							|  |  |  | 				.filter(function(p){  | 
					
						
							|  |  |  | 					return p != '' }) | 
					
						
							|  |  |  | 				.shift() | 
					
						
							|  |  |  | 			return fs.promises.readdir(base +'/'+ sub) | 
					
						
							|  |  |  | 				.iter() | 
					
						
							|  |  |  | 				// skip backups...
 | 
					
						
							|  |  |  | 				.filter(function(file){ | 
					
						
							|  |  |  | 					return !file.includes(b) }) | 
					
						
							|  |  |  | 				.map(async function(file){ | 
					
						
							|  |  |  | 					return await that.create(base, sub +'/'+ file, version, options) }) | 
					
						
							|  |  |  | 				// keep only the paths we backed up...
 | 
					
						
							|  |  |  | 				.filter(function(e){  | 
					
						
							|  |  |  | 					return !!e })  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// backup single page...
 | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			var target = pwpath.join(base, sub)  | 
					
						
							|  |  |  | 			var full = _backup[0] == '/' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// nothing to backup...
 | 
					
						
							|  |  |  | 			if(!fs.existsSync(target)){ | 
					
						
							|  |  |  | 				verbose | 
					
						
							|  |  |  | 					&& console.log('.create(..): target does not exist:', target) | 
					
						
							|  |  |  | 				return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			var to = full ? | 
					
						
							|  |  |  | 				backup +'/'+ sub | 
					
						
							|  |  |  | 				: backup +'/'+ pwpath.basename(sub) | 
					
						
							|  |  |  | 			var todir = pwpath.dirname(to) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if(!recursive){ | 
					
						
							|  |  |  | 				var stat = await fs.promises.stat(target) | 
					
						
							|  |  |  | 				if(stat.isDirectory()){ | 
					
						
							|  |  |  | 					target += '/'+index  | 
					
						
							|  |  |  | 					to += '/'+index  | 
					
						
							|  |  |  | 					// nothing to backup...
 | 
					
						
							|  |  |  | 					if(!fs.existsSync(target)){ | 
					
						
							|  |  |  | 						verbose | 
					
						
							|  |  |  | 							&& !__batch | 
					
						
							|  |  |  | 							&& console.log('.create(..): nothing to backup:', target) | 
					
						
							|  |  |  | 						return } } } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			verbose | 
					
						
							|  |  |  | 				&& console.log('.create(..):', sub, '->', to) | 
					
						
							|  |  |  | 			await fs.promises.mkdir(todir, {recursive: true})  | 
					
						
							|  |  |  | 			await fs.promises.cp(target, to, {force: true, recursive}) | 
					
						
							|  |  |  | 			return to } }, | 
					
						
							|  |  |  | 	restore: async function(base, sub, version, options){ | 
					
						
							|  |  |  | 		var that = this | 
					
						
							|  |  |  | 		// XXX
 | 
					
						
							|  |  |  | 		var {index, backup, verbose, recursive, preBackup, __batch} = options = getOpts(options) | 
					
						
							|  |  |  | 		recursive = recursive ?? false | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		var _backup = backup =  | 
					
						
							|  |  |  | 			version ? | 
					
						
							|  |  |  | 				pwpath.join(backup, version) | 
					
						
							|  |  |  | 				: backup | 
					
						
							|  |  |  | 		backup =  | 
					
						
							|  |  |  | 			pwpath.join( | 
					
						
							|  |  |  | 				base, | 
					
						
							|  |  |  | 				pwpath.relative( | 
					
						
							|  |  |  | 					pwpath.dirname(sub),  | 
					
						
							|  |  |  | 					backup)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// check if we can restore...
 | 
					
						
							|  |  |  | 		if(!fs.existsSync(backup)){ | 
					
						
							|  |  |  | 			verbose | 
					
						
							|  |  |  | 				&& console.log('restore(..): no backup version:', version) | 
					
						
							|  |  |  | 			return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// XXX should we use the same options...
 | 
					
						
							|  |  |  | 		preBackup | 
					
						
							|  |  |  | 			&& await this.create(base, sub, options ?? {}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// ** or * -- backup each file in path...
 | 
					
						
							|  |  |  | 		// NOTE: when restoring there is no difference between ** and *...
 | 
					
						
							|  |  |  | 		if(/[\\\/]*\*\*?$/.test(sub)){ | 
					
						
							|  |  |  | 			if(sub.endsWith('**')){ | 
					
						
							|  |  |  | 				options.recursive = true } | 
					
						
							|  |  |  | 			// restore...
 | 
					
						
							|  |  |  | 			// NOTE: we have already made a full backup so no need to 
 | 
					
						
							|  |  |  | 			// 		redo it down the line...
 | 
					
						
							|  |  |  | 			options.preBackup = false | 
					
						
							|  |  |  | 			options.__batch = true | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			sub = sub.replace(/[\\\/]*\*\*?$/, '') | 
					
						
							|  |  |  | 			var to = pwpath.join(base, sub) | 
					
						
							|  |  |  | 			var b = pwpath.split(_backup) | 
					
						
							|  |  |  | 				.filter(function(p){  | 
					
						
							|  |  |  | 					return p != '' }) | 
					
						
							|  |  |  | 				.shift() | 
					
						
							|  |  |  | 			// cleanup...
 | 
					
						
							|  |  |  | 			// NOTE: we need this stage as the file list we are backing up 
 | 
					
						
							|  |  |  | 			// 		and the one in the target dir can differ, and a single-page
 | 
					
						
							|  |  |  | 			// 		.restore(..) will only remove collisions...
 | 
					
						
							|  |  |  | 			await fs.promises.readdir(base +'/'+ sub) | 
					
						
							|  |  |  | 				.iter() | 
					
						
							|  |  |  | 				// skip backups...
 | 
					
						
							|  |  |  | 				.filter(function(file){ | 
					
						
							|  |  |  | 					return !file.includes(b) }) | 
					
						
							|  |  |  | 				.map(async function(file){ | 
					
						
							|  |  |  | 					var p = pwpath.join(base, sub, file) | 
					
						
							|  |  |  | 					verbose | 
					
						
							|  |  |  | 						&& console.log('restore(..): removing:', p) | 
					
						
							|  |  |  | 					await fs.promises.rm(p, {recursive: true}) | 
					
						
							|  |  |  | 					return p }) | 
					
						
							|  |  |  | 			return fs.promises.readdir(backup) | 
					
						
							|  |  |  | 				.iter() | 
					
						
							|  |  |  | 				.map(async function(file){ | 
					
						
							|  |  |  | 					return await that.restore(base, sub+'/'+file, version, options) }) | 
					
						
							|  |  |  | 				// keep only the paths we backed up...
 | 
					
						
							|  |  |  | 				.filter(function(e){  | 
					
						
							|  |  |  | 					return !!e })  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// single page...
 | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			var index_file = '' | 
					
						
							|  |  |  | 			var full = _backup[0] == '/' | 
					
						
							|  |  |  | 			var source = full ? | 
					
						
							|  |  |  | 				pwpath.join(backup, sub) | 
					
						
							|  |  |  | 				: pwpath.join(backup, pwpath.basename(sub)) | 
					
						
							|  |  |  | 			if(!fs.existsSync(source)){ | 
					
						
							|  |  |  | 				verbose | 
					
						
							|  |  |  | 					&& console.log('restore(..): source not present in backup:', source) | 
					
						
							|  |  |  | 				return } | 
					
						
							|  |  |  | 			var to = pwpath.join(base, sub) | 
					
						
							|  |  |  | 			if(fs.existsSync(to)){ | 
					
						
							|  |  |  | 				var stat = await fs.promises.stat(to) | 
					
						
							|  |  |  | 				if(stat.isDirectory()){ | 
					
						
							|  |  |  | 					var f = pwpath.join(to, index) | 
					
						
							|  |  |  | 					if(fs.existsSync(f)){ | 
					
						
							|  |  |  | 						verbose | 
					
						
							|  |  |  | 							&& console.log('restore(..): removing:', f) | 
					
						
							|  |  |  | 						await fs.promises.rm(f) } | 
					
						
							|  |  |  | 				} else { | 
					
						
							|  |  |  | 					verbose | 
					
						
							|  |  |  | 						&& console.log('restore(..): removing:', to) | 
					
						
							|  |  |  | 					await fs.promises.rm(to) } } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if(!recursive){ | 
					
						
							|  |  |  | 				// handle dir text...
 | 
					
						
							|  |  |  | 				var stat = await fs.promises.stat(source) | 
					
						
							|  |  |  | 				if(stat.isDirectory()){ | 
					
						
							|  |  |  | 					source += '/'+index | 
					
						
							|  |  |  | 					to += '/'+index  | 
					
						
							|  |  |  | 					if(!fs.existsSync(source)){ | 
					
						
							|  |  |  | 						verbose | 
					
						
							|  |  |  | 							&& !__batch | 
					
						
							|  |  |  | 							&& console.log('restore(..): source not present in backup:', source) | 
					
						
							|  |  |  | 						return } } } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			verbose | 
					
						
							|  |  |  | 				&& console.log('restore(..): restoring:', to) | 
					
						
							|  |  |  | 			await fs.promises.cp(source, to, {recursive: true}) | 
					
						
							|  |  |  | 			return source } }, | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	//	Get backup versions...
 | 
					
						
							|  |  |  | 	//	listbackups(<base>[, <options>])
 | 
					
						
							|  |  |  | 	//	listbackups(<base>, '*'[, <options>])
 | 
					
						
							|  |  |  | 	//		-> <list>
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	//	Get backup versions containing <path>...
 | 
					
						
							|  |  |  | 	//	listbackups(<base>, <path>[, <options>])
 | 
					
						
							|  |  |  | 	//		-> <list>
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	list: async function(base, sub, options){ | 
					
						
							|  |  |  | 		var that = this | 
					
						
							|  |  |  | 		if(typeof(sub) == 'object'){ | 
					
						
							|  |  |  | 			options = sub | 
					
						
							|  |  |  | 			sub = '*' } | 
					
						
							|  |  |  | 		var {backup} = getOpts(options) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// handle local/global backups...
 | 
					
						
							|  |  |  | 		var full = backup[0] == '/' | 
					
						
							|  |  |  | 		base = full ? | 
					
						
							|  |  |  | 			pwpath.join(base, backup) | 
					
						
							|  |  |  | 			: pwpath.join(base, pwpath.dirname(sub), backup) | 
					
						
							|  |  |  | 		sub = full ? | 
					
						
							|  |  |  | 			sub | 
					
						
							|  |  |  | 			: pwpath.basename(sub) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return fs.existsSync(base) ? | 
					
						
							|  |  |  | 			fs.promises.readdir(base) | 
					
						
							|  |  |  | 				.iter() | 
					
						
							|  |  |  | 				.filter(function(version){ | 
					
						
							|  |  |  | 					return (sub == '*' || sub == '**') | 
					
						
							|  |  |  | 						|| fs.existsSync( | 
					
						
							|  |  |  | 							pwpath.join(base, version, sub)) })  | 
					
						
							|  |  |  | 			: [] }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	remove: async function(base, version, options){ | 
					
						
							|  |  |  | 		var {backup, verbose} = getOpts(options) | 
					
						
							|  |  |  | 		var target =  | 
					
						
							|  |  |  | 			(version == '*' || version == '**') ? | 
					
						
							|  |  |  | 				pwpath.join(base, backup) | 
					
						
							|  |  |  | 				: pwpath.join(base, backup, version) | 
					
						
							|  |  |  | 		if(fs.existsSync(target)){ | 
					
						
							|  |  |  | 			verbose | 
					
						
							|  |  |  | 				&& console.log(`.remove(..): removing:`, target) | 
					
						
							|  |  |  | 			await fs.promises.rm(target, {recursive: true}) | 
					
						
							|  |  |  | 			return target } }, | 
					
						
							|  |  |  | 	clear: async function(base, options){ | 
					
						
							|  |  |  | 		return await this.remove(base, '*', options) } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // XXX might be a good idea to support ro mode on top level explicitly...
 | 
					
						
							|  |  |  | // XXX add monitor API + cache + live mode (auto on when lock detected)...
 | 
					
						
							|  |  |  | var FileStoreRO = | 
					
						
							|  |  |  | module.FileStoreRO = { | 
					
						
							|  |  |  | 	__proto__: base.BaseStore, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// XXX
 | 
					
						
							|  |  |  | 	__path__: 'data/fs', | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// XXX should this be "index" or ".index"???
 | 
					
						
							|  |  |  | 	__directory_text__: '.index', | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// XXX do we remove the extension???
 | 
					
						
							|  |  |  | 	// XXX cache???
 | 
					
						
							|  |  |  | 	__paths__: async function(){ | 
					
						
							|  |  |  | 		var that = this | 
					
						
							|  |  |  | 		return new Promise(function(resolve, reject){ | 
					
						
							|  |  |  | 			glob(pwpath.join(that.__path__, '**/*')) | 
					
						
							|  |  |  | 				.on('end', function(paths){ | 
					
						
							|  |  |  | 					Promise.all(paths | 
					
						
							|  |  |  | 							.map(async function(path){ | 
					
						
							|  |  |  | 								return await module.exists(path) ? | 
					
						
							|  |  |  | 									path | 
					
						
							|  |  |  | 										.slice(that.__path__.length) | 
					
						
							|  |  |  | 									: [] })) | 
					
						
							|  |  |  | 						.then(function(paths){ | 
					
						
							|  |  |  | 							resolve(paths.flat()) }) }) }) }, | 
					
						
							|  |  |  | 	__exists__: async function(path){ | 
					
						
							|  |  |  | 		return await module.exists(this.__path__, path, {index: this.__directory_text__})  | 
					
						
							|  |  |  | 			&& path }, | 
					
						
							|  |  |  | 	__get__: async function(path){ | 
					
						
							|  |  |  | 		var p = pwpath.join(this.__path__, path) | 
					
						
							|  |  |  | 		var {atimeMs, mtimeMs, ctimeMs, birthtimeMs} = await fs.promises.stat(p) | 
					
						
							|  |  |  | 		return { | 
					
						
							|  |  |  | 			atime: atimeMs, | 
					
						
							|  |  |  | 			mtime: mtimeMs, | 
					
						
							|  |  |  | 			ctime: ctimeMs, | 
					
						
							|  |  |  | 			text: await module.read(p, {index: this.__directory_text__}) | 
					
						
							|  |  |  | 		} }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	__update__: function(){}, | 
					
						
							|  |  |  | 	__delete__: function(){}, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // XXX add a lock file and prevent multiple adapters from controlling 
 | 
					
						
							|  |  |  | // 		one path...
 | 
					
						
							|  |  |  | // XXX backup files on write/delete...
 | 
					
						
							|  |  |  | var FileStore = | 
					
						
							|  |  |  | module.FileStore = { | 
					
						
							|  |  |  | 	__proto__: FileStoreRO, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// XXX
 | 
					
						
							|  |  |  | 	__path__: 'data/fs', | 
					
						
							|  |  |  | 	__backup_path__: '/.pwiki/backup', | 
					
						
							|  |  |  | 	__lock_path__: '/.pwiki/lock', | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// XXX should this be "index" or ".index"???
 | 
					
						
							|  |  |  | 	__directory_text__: '.index', | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	__clear_lock__: [ | 
					
						
							|  |  |  | 		`SIGINT`,  | 
					
						
							|  |  |  | 		`SIGUSR1`,  | 
					
						
							|  |  |  | 		`SIGUSR2`,  | 
					
						
							|  |  |  | 		`SIGTERM`, | 
					
						
							|  |  |  | 		`exit`,  | 
					
						
							|  |  |  | 		// XXX should we handle this??
 | 
					
						
							|  |  |  | 		// 		...this can be an indicator of inconsistent state...
 | 
					
						
							|  |  |  | 		//`uncaughtException`, 
 | 
					
						
							|  |  |  | 	], | 
					
						
							|  |  |  | 	__exit_lock_handler: undefined, | 
					
						
							|  |  |  | 	// prevent more than one handler to write to a store...
 | 
					
						
							|  |  |  | 	ensureLock: async function(){ | 
					
						
							|  |  |  | 		var that = this | 
					
						
							|  |  |  | 		var lock = this.__path__ + this.__lock_path__ | 
					
						
							|  |  |  | 		// check lock...
 | 
					
						
							|  |  |  | 		if(fs.existsSync(lock)){ | 
					
						
							|  |  |  | 			if(await module.read(lock) != process.pid){ | 
					
						
							|  |  |  | 				throw new Error('attempting to write to a locked store:', this.__path__) } | 
					
						
							|  |  |  | 		// set lock...
 | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			module.update(lock, `${process.pid}`)  | 
					
						
							|  |  |  | 			this.__exit_lock_handler =  | 
					
						
							|  |  |  | 				this.__exit_lock_handler  | 
					
						
							|  |  |  | 					// NOTE: this must be sync as deferred calls might 
 | 
					
						
							|  |  |  | 					// 		not get a chance to execute...
 | 
					
						
							|  |  |  | 					?? function(){ | 
					
						
							|  |  |  | 						fs.rmSync(lock) } | 
					
						
							|  |  |  | 			this.__clear_lock__.forEach(function(evt){ | 
					
						
							|  |  |  | 				process.off(evt, that.__exit_lock_handler) | 
					
						
							|  |  |  | 				process.on(evt, that.__exit_lock_handler) }) } | 
					
						
							|  |  |  | 		return this }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// XXX do we write all the data or only the .text???
 | 
					
						
							|  |  |  | 	__update__: async function(path, data, mode='update'){ | 
					
						
							|  |  |  | 		this.ensureLock() | 
					
						
							|  |  |  | 		return module.update( | 
					
						
							|  |  |  | 			this.__path__, path,  | 
					
						
							|  |  |  | 			data.text,  | 
					
						
							|  |  |  | 			{index: this.__directory_text__}) }, | 
					
						
							|  |  |  |     __delete__: async function(path){ | 
					
						
							|  |  |  | 		this.ensureLock() | 
					
						
							|  |  |  | 		return module.clear( | 
					
						
							|  |  |  | 			this.__path__, path,  | 
					
						
							|  |  |  | 			{index: this.__directory_text__}) }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// specific API...
 | 
					
						
							|  |  |  | 	cleanup: async function(options={}){ | 
					
						
							|  |  |  | 		return module.cleanup(this.__path__, { | 
					
						
							|  |  |  | 			index: this.__directory_text__, | 
					
						
							|  |  |  | 			...options,  | 
					
						
							|  |  |  | 		}) }, | 
					
						
							|  |  |  | 	// XXX add explicit versioning???
 | 
					
						
							|  |  |  | 	backup: async function(path='**', options={}){ | 
					
						
							|  |  |  | 		this.ensureLock() | 
					
						
							|  |  |  | 		return backup.create( | 
					
						
							|  |  |  | 			this.__path__, path,  | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				index: this.__directory_text__, | 
					
						
							|  |  |  | 				backup: this.__backup_path__, | 
					
						
							|  |  |  | 				...options, | 
					
						
							|  |  |  | 			}) }, | 
					
						
							|  |  |  | 	restore: async function(path='**', options={}){ | 
					
						
							|  |  |  | 		this.ensureLock() | 
					
						
							|  |  |  | 		return backup.restore( | 
					
						
							|  |  |  | 			this.__path__, path,  | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				index: this.__directory_text__, | 
					
						
							|  |  |  | 				backup: this.__backup_path__, | 
					
						
							|  |  |  | 				...options, | 
					
						
							|  |  |  | 			}) }, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /********************************************************************** | 
					
						
							|  |  |  | * vim:set ts=4 sw=4 :                               */ return module }) |