mirror of
				https://github.com/flynx/pWiki.git
				synced 2025-10-31 19:10:08 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			712 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			712 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| /**********************************************************************
 | |
| * 
 | |
| *
 | |
| *
 | |
| **********************************************************************/
 | |
| ((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 cp = require('child_process')
 | |
| 
 | |
| var object = require('ig-object')
 | |
| var types = require('ig-types')
 | |
| 
 | |
| var pwpath = require('../path')
 | |
| var base = require('./base')
 | |
| 
 | |
| 
 | |
| 
 | |
| //---------------------------------------------------------------------
 | |
| 
 | |
| //
 | |
| // 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 ?? {}),
 | |
| 	} }
 | |
| 
 | |
| 
 | |
| var encode = 
 | |
| module.encode =
 | |
| function(str){
 | |
| 	return str.replace(/[^\w .\\\/_\-]/gi, 
 | |
| 		function(c){
 | |
| 			return `%${c
 | |
| 				.charCodeAt(0)
 | |
| 				.toString(16)
 | |
| 				.toUpperCase()}` }) }
 | |
| var decode =
 | |
| module.decode =
 | |
| function(str){
 | |
| 	return decodeURIComponent(str) }
 | |
| 
 | |
| 
 | |
| //	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)
 | |
| 
 | |
| 	sub = pwpath.sanitize(sub)
 | |
| 	var target = encode(
 | |
| 		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)
 | |
| 
 | |
| 	sub = pwpath.sanitize(sub)
 | |
| 	var target = encode(
 | |
| 		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' 
 | |
| 			&& !(sub instanceof Array)){
 | |
| 		options = sub ?? options
 | |
| 		sub = base 
 | |
| 		base = null }
 | |
| 	var {index, verbose} = getOpts(options)
 | |
| 
 | |
| 	sub = pwpath.sanitize(sub)
 | |
| 	var levels = pwpath.split(sub)
 | |
| 	for(var level of levels){
 | |
| 		base = encode(
 | |
| 			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 }
 | |
| 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)
 | |
| 
 | |
| 	sub = pwpath.sanitize(sub)
 | |
| 	var target = encode(
 | |
| 		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 {
 | |
| 		// NOTE: no need to encode stuff here as it will be taken care 
 | |
| 		// 		of by .mkdir(..)
 | |
| 		var levels = pwpath.split(sub)
 | |
| 		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)
 | |
| 
 | |
| 	sub = pwpath.sanitize(sub)
 | |
| 	// remove leaf...
 | |
| 	var target = encode(
 | |
| 		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(encode(sub))
 | |
| 		.slice(0, -1)
 | |
| 	base = encode(base)
 | |
| 	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(encode(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.Store,
 | |
| 
 | |
| 	// XXX
 | |
| 	__path__: 'data/fs',
 | |
| 	__pwiki_path__: '/.pwiki/',
 | |
| 	__metadata_path__: '$PWIKI/metadata',
 | |
| 
 | |
| 	// XXX should this be "index" or ".index"???
 | |
| 	__directory_text__: '.index',
 | |
| 
 | |
| 	expandPWikiPath: function(...path){
 | |
| 		return pwpath.join(...path)
 | |
| 			.replace(/\$PWIKI/, this.__pwiki_path__) },
 | |
| 
 | |
| 	// 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) ?
 | |
| 									decode(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 m = this.expandPWikiPath(this.__path__, this.__metadata_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__}),
 | |
| 			...JSON.parse(await module.read(m, {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',
 | |
| 	__pwiki_path__: '/.pwiki',
 | |
| 	__backup_path__: '$PWIKI/backup',
 | |
| 	__lock_path__: '$PWIKI/lock',
 | |
| 
 | |
| 	__directory_text__: '.index',
 | |
| 
 | |
| 	// prevent more than one handler to write to a store...
 | |
| 	//
 | |
| 	__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,
 | |
| 	ensureLock: async function(){
 | |
| 		var that = this
 | |
| 		var lock = this.expandPWikiPath(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 {
 | |
| 			await module.update(lock, `${process.pid}`) 
 | |
| 			// keep the pwiki dir hidden on windows...
 | |
| 			if(process.platform == 'win32'){
 | |
| 				cp.execSync('attrib +h '+ 
 | |
| 					this.expandPWikiPath(this.__path__, this.__pwiki_path__)) }
 | |
| 			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()
 | |
| 		// metadata...
 | |
| 		module.update(
 | |
| 			this.__path__,
 | |
| 			this.expandPWikiPath(this.__metadata_path__, path),
 | |
| 			JSON.stringify(data),
 | |
| 			{index: this.__directory_text__})
 | |
| 		// text...
 | |
| 		return module.update(
 | |
| 			this.__path__, path, 
 | |
| 			data.text, 
 | |
| 			{index: this.__directory_text__}) },
 | |
|     __delete__: async function(path){
 | |
| 		this.ensureLock()
 | |
| 		// metadata...
 | |
| 		module.clear(
 | |
| 			this.__path__,
 | |
| 			this.expandPWikiPath(this.__metadata_path__, path),
 | |
| 			{index: this.__directory_text__})
 | |
| 		// text...
 | |
| 		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.expandPWikiPath(this.__backup_path__),
 | |
| 				...options,
 | |
| 			}) },
 | |
| 	restore: async function(path='**', options={}){
 | |
| 		this.ensureLock()
 | |
| 		return backup.restore(
 | |
| 			this.__path__, path, 
 | |
| 			{
 | |
| 				index: this.__directory_text__,
 | |
| 				backup: this.expandPWikiPath(this.__backup_path__),
 | |
| 				...options,
 | |
| 			}) },
 | |
| }
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
| /**********************************************************************
 | |
| * vim:set ts=4 sw=4 :                               */ return module })
 |