diff --git a/lib/AbstractReaderWriter.js b/lib/AbstractReaderWriter.js
index 9b8f75ac..11ba90a0 100644
--- a/lib/AbstractReaderWriter.js
+++ b/lib/AbstractReaderWriter.js
@@ -25,11 +25,22 @@ class AbstractReaderWriter extends AbstractReader {
* Writes the content of a resource to a path.
*
* @public
- * @param {module:@ui5/fs.Resource} resource The Resource to write
+ * @param {module:@ui5/fs.Resource} resource Resource to write
+ * @param {Object} [options]
+ * @param {boolean} [options.readOnly=false] Whether the resource content shall be written read-only
+ * Do not use in conjunction with the drain
option.
+ * The written file will be used as the new source of this resources content.
+ * Therefore the written file should not be altered by any means.
+ * Activating this option might improve overall memory consumption.
+ * @param {boolean} [options.drain=false] Whether the resource content shall be emptied during the write process.
+ * Do not use in conjunction with the readOnly
option.
+ * Activating this option might improve overall memory consumption.
+ * This should be used in cases where this is the last access to the resource.
+ * E.g. the final write of a resource after all processing is finished.
* @returns {Promise} Promise resolving once data has been written
*/
- write(resource) {
- return this._write(resource);
+ write(resource, options = {drain: false, readOnly: false}) {
+ return this._write(resource, options);
}
/**
@@ -37,10 +48,11 @@ class AbstractReaderWriter extends AbstractReader {
*
* @abstract
* @protected
- * @param {module:@ui5/fs.Resource} resource The Resource to write
+ * @param {module:@ui5/fs.Resource} resource Resource to write
+ * @param {Object} [options] Write options, see above
* @returns {Promise} Promise resolving once data has been written
*/
- _write(resource) {
+ _write(resource, options) {
throw new Error("Not implemented");
}
}
diff --git a/lib/Resource.js b/lib/Resource.js
index 69043da1..599a6bf4 100644
--- a/lib/Resource.js
+++ b/lib/Resource.js
@@ -12,21 +12,32 @@ const fnFalse = () => false;
* @memberof module:@ui5/fs
*/
class Resource {
+ /**
+ * Function for dynamic creation of content streams
+ *
+ * @public
+ * @callback module:@ui5/fs.Resource~createStream
+ * @returns {stream.Readable} A readable stream of a resources content
+ */
+
/**
* The constructor.
*
* @public
* @param {Object} parameters Parameters
* @param {string} parameters.path Virtual path
- * @param {Object} [parameters.statInfo] File stat information
+ * @param {fs.Stats|Object} [parameters.statInfo] File information. Instance of
+ * [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats} or similar object
* @param {Buffer} [parameters.buffer] Content of this resources as a Buffer instance
* (cannot be used in conjunction with parameters string, stream or createStream)
* @param {string} [parameters.string] Content of this resources as a string
* (cannot be used in conjunction with parameters buffer, stream or createStream)
* @param {Stream} [parameters.stream] Readable stream of the content of this resource
* (cannot be used in conjunction with parameters buffer, string or createStream)
- * @param {Function} [parameters.createStream] Function callback that returns a readable stream of the content
- * of this resource (cannot be used in conjunction with parameters buffer, string or stream)
+ * @param {module:@ui5/fs.Resource~createStream} [parameters.createStream] Function callback that returns a readable
+ * stream of the content of this resource (cannot be used in conjunction with parameters buffer,
+ * string or stream).
+ * In some cases this is the most memory-efficient way to supply resource content
*/
constructor({path, statInfo, buffer, string, createStream, stream, project}) {
if (!path) {
@@ -74,25 +85,28 @@ class Resource {
* Gets a buffer with the resource content.
*
* @public
- * @returns {Promise} A Promise resolving with a buffer of the resource content.
+ * @returns {Promise} Promise resolving with a buffer of the resource content.
*/
- getBuffer() {
- return new Promise((resolve, reject) => {
- if (this._buffer) {
- resolve(this._buffer);
- } else if (this._createStream || this._stream) {
- resolve(this._getBufferFromStream());
- } else {
- reject(new Error(`Resource ${this._path} has no content`));
- }
- });
+ async getBuffer() {
+ if (this._contentDrained) {
+ throw new Error(`Content of Resource ${this._path} has been drained. ` +
+ "This might be caused by requesting resource content after a content stream has been " +
+ "requested and no new content (e.g. a new stream) has been set.");
+ }
+ if (this._buffer) {
+ return this._buffer;
+ } else if (this._createStream || this._stream) {
+ return this._getBufferFromStream();
+ } else {
+ throw new Error(`Resource ${this._path} has no content`);
+ }
}
/**
* Sets a Buffer as content.
*
* @public
- * @param {Buffer} buffer A buffer instance
+ * @param {Buffer} buffer Buffer instance
*/
setBuffer(buffer) {
this._createStream = null;
@@ -101,15 +115,22 @@ class Resource {
// }
this._stream = null;
this._buffer = buffer;
+ this._contentDrained = false;
+ this._streamDrained = false;
}
/**
* Gets a string with the resource content.
*
* @public
- * @returns {Promise} A Promise resolving with a string of the resource content.
+ * @returns {Promise} Promise resolving with the resource content.
*/
getString() {
+ if (this._contentDrained) {
+ return Promise.reject(new Error(`Content of Resource ${this._path} has been drained. ` +
+ "This might be caused by requesting resource content after a content stream has been " +
+ "requested and no new content (e.g. a new stream) has been set."));
+ }
return this.getBuffer().then((buffer) => buffer.toString());
}
@@ -117,7 +138,7 @@ class Resource {
* Sets a String as content
*
* @public
- * @param {string} string A string
+ * @param {string} string Resource content
*/
setString(string) {
this.setBuffer(Buffer.from(string, "utf8"));
@@ -126,34 +147,64 @@ class Resource {
/**
* Gets a readable stream for the resource content.
*
+ * Repetitive calls of this function are only possible if new content has been set in the meantime (through
+ * [setStream]{@link module:@ui5/fs.Resource#setStream}, [setBuffer]{@link module:@ui5/fs.Resource#setBuffer}
+ * or [setString]{@link module:@ui5/fs.Resource#setString}). This
+ * is to prevent consumers from accessing drained streams.
+ *
* @public
- * @returns {stream.Readable} A readable stream for the resource content.
+ * @returns {stream.Readable} Readable stream for the resource content.
*/
getStream() {
+ if (this._contentDrained) {
+ throw new Error(`Content of Resource ${this._path} has been drained. ` +
+ "This might be caused by requesting resource content after a content stream has been " +
+ "requested and no new content (e.g. a new stream) has been set.");
+ }
+ let contentStream;
if (this._buffer) {
const bufferStream = new stream.PassThrough();
bufferStream.end(this._buffer);
- return bufferStream;
+ contentStream = bufferStream;
} else if (this._createStream || this._stream) {
- return this._getStream();
- } else {
+ contentStream = this._getStream();
+ }
+ if (!contentStream) {
throw new Error(`Resource ${this._path} has no content`);
}
+ // If a stream instance is being returned, it will typically get drained be the consumer.
+ // In that case, further content access will result in a "Content stream has been drained" error.
+ // However, depending on the execution environment, a resources content stream might have been
+ // transformed into a buffer. In that case further content access is possible as a buffer can't be
+ // drained.
+ // To prevent unexpected "Content stream has been drained" errors caused by changing environments, we flag
+ // the resource content as "drained" every time a stream is requested. Even if actually a buffer or
+ // createStream callback is being used.
+ this._contentDrained = true;
+ return contentStream;
}
/**
* Sets a readable stream as content.
*
* @public
- * @param {stream.Readable} stream readable stream
+ * @param {stream.Readable|module:@ui5/fs.Resource~createStream} stream Readable stream of the resource content or
+ callback for dynamic creation of a readable stream
*/
setStream(stream) {
this._buffer = null;
- this._createStream = null;
// if (this._stream) { // TODO this may cause strange issues
// this._stream.destroy();
// }
- this._stream = stream;
+ if (typeof stream === "function") {
+ this._createStream = stream;
+ this._stream = null;
+ } else {
+ this._stream = stream;
+ this._createStream = null;
+ }
+ this._contentDrained = false;
+ this._streamDrained = false;
}
/**
@@ -181,7 +232,8 @@ class Resource {
* Gets the resources stat info.
*
* @public
- * @returns {fs.Stats} An object representing an fs.Stats instance
+ * @returns {fs.Stats|Object} Instance of [fs.Stats]{@link https://nodejs.org/api/fs.html#fs_class_fs_stats}
+ * or similar object
*/
getStatInfo() {
return this._statInfo;
@@ -201,10 +253,10 @@ class Resource {
}
/**
- * Returns a clone of the resource.
+ * Returns a clone of the resource. The clones content is independent from that of the original resource
*
* @public
- * @returns {Promise} A promise resolving the resource.
+ * @returns {Promise} Promise resolving with the clone
*/
clone() {
const options = {
@@ -235,7 +287,7 @@ class Resource {
/**
* Tracing: Get tree for printing out trace
*
- * @returns {Object}
+ * @returns {Object} Trace tree
*/
getPathTree() {
const tree = {};
@@ -253,12 +305,16 @@ class Resource {
* Returns the content as stream.
*
* @private
- * @returns {Function} The stream
+ * @returns {stream.Readable} Readable stream
*/
_getStream() {
+ if (this._streamDrained) {
+ throw new Error(`Content stream of Resource ${this._path} is flagged as drained.`);
+ }
if (this._createStream) {
return this._createStream();
}
+ this._streamDrained = true;
return this._stream;
}
@@ -269,7 +325,10 @@ class Resource {
* @returns {Promise} Promise resolving with buffer.
*/
_getBufferFromStream() {
- return new Promise((resolve, reject) => {
+ if (this._buffering) { // Prevent simultaneous buffering, causing unexpected access to drained stream
+ return this._buffering;
+ }
+ return this._buffering = new Promise((resolve, reject) => {
const contentStream = this._getStream();
const buffers = [];
contentStream.on("data", (data) => {
@@ -281,6 +340,7 @@ class Resource {
contentStream.on("end", () => {
const buffer = Buffer.concat(buffers);
this.setBuffer(buffer);
+ this._buffering = null;
resolve(buffer);
});
});
diff --git a/lib/adapters/FileSystem.js b/lib/adapters/FileSystem.js
index 3f60dd43..98c076aa 100644
--- a/lib/adapters/FileSystem.js
+++ b/lib/adapters/FileSystem.js
@@ -3,6 +3,7 @@ const path = require("path");
const fs = require("graceful-fs");
const glob = require("globby");
const makeDir = require("make-dir");
+const {PassThrough} = require("stream");
const Resource = require("../Resource");
const AbstractAdapter = require("./AbstractAdapter");
@@ -153,7 +154,7 @@ class FileSystem extends AbstractAdapter {
if (!stat.isDirectory()) {
// Add content
- options.createStream = () => {
+ options.createStream = function() {
return fs.createReadStream(fsPath);
};
}
@@ -168,33 +169,79 @@ class FileSystem extends AbstractAdapter {
* Writes the content of a resource to a path.
*
* @private
- * @param {module:@ui5/fs.Resource} resource The Resource
+ * @param {module:@ui5/fs.Resource} resource Resource to write
+ * @param {Object} [options]
+ * @param {boolean} [options.readOnly] Whether the resource content shall be written read-only
+ * Do not use in conjunction with the drain
option.
+ * The written file will be used as the new source of this resources content.
+ * Therefore the written file should not be altered by any means.
+ * Activating this option might improve overall memory consumption.
+ * @param {boolean} [options.drain] Whether the resource content shall be emptied during the write process.
+ * Do not use in conjunction with the readOnly
option.
+ * Activating this option might improve overall memory consumption.
+ * This should be used in cases where this is the last access to the resource.
+ * E.g. the final write of a resource after all processing is finished.
* @returns {Promise} Promise resolving once data has been written
*/
- _write(resource) {
+ async _write(resource, {drain, readOnly}) {
+ if (drain && readOnly) {
+ throw new Error(`Error while writing resource ${resource.getPath()}: ` +
+ "Do not use options 'drain' and 'readOnly' at the same time.");
+ }
+
const relPath = resource.getPath().substr(this._virBasePath.length);
const fsPath = path.join(this._fsBasePath, relPath);
const dirPath = path.dirname(fsPath);
log.verbose("Writing to %s", fsPath);
- return makeDir(dirPath, {
- fs
- }).then(() => {
- return new Promise((resolve, reject) => {
- const contentStream = resource.getStream();
- contentStream.on("error", function(err) {
+ await makeDir(dirPath, {fs});
+ return new Promise((resolve, reject) => {
+ let contentStream;
+
+ if (drain || readOnly) {
+ // Stream will be drained
+ contentStream = resource.getStream();
+
+ contentStream.on("error", (err) => {
reject(err);
});
- const write = fs.createWriteStream(fsPath);
- write.on("error", function(err) {
+ } else {
+ // Transform stream into buffer before writing
+ contentStream = new PassThrough();
+ const buffers = [];
+ contentStream.on("error", (err) => {
reject(err);
});
- write.on("close", function(ex) {
- resolve();
+ contentStream.on("data", (data) => {
+ buffers.push(data);
+ });
+ contentStream.on("end", () => {
+ const buffer = Buffer.concat(buffers);
+ resource.setBuffer(buffer);
});
- contentStream.pipe(write);
+ resource.getStream().pipe(contentStream);
+ }
+
+ const writeOptions = {};
+ if (readOnly) {
+ writeOptions.mode = 0o444; // read only
+ }
+
+ const write = fs.createWriteStream(fsPath, writeOptions);
+ write.on("error", (err) => {
+ reject(err);
+ });
+ write.on("close", (ex) => {
+ if (readOnly) {
+ // Create new stream from written file
+ resource.setStream(function() {
+ return fs.createReadStream(fsPath);
+ });
+ }
+ resolve();
});
+ contentStream.pipe(write);
});
}
}
diff --git a/test/lib/Resource.js b/test/lib/Resource.js
index d7e7f5a5..193efb89 100644
--- a/test/lib/Resource.js
+++ b/test/lib/Resource.js
@@ -1,7 +1,23 @@
const {test} = require("ava");
const Stream = require("stream");
+const fs = require("fs");
+const path = require("path");
const Resource = require("../../lib/Resource");
+function createBasicResource() {
+ const fsPath = path.join("test", "fixtures", "application.a", "webapp", "index.html");
+ const resource = new Resource({
+ path: "/app/index.html",
+ createStream: function() {
+ return fs.createReadStream(fsPath);
+ },
+ project: {},
+ statInfo: {},
+ fsPath
+ });
+ return resource;
+}
+
test("Resource: constructor with missing path parameter", (t) => {
const error = t.throws(() => {
new Resource({});
@@ -140,3 +156,72 @@ test("Resource: clone resource with stream", (t) => {
});
});
});
+
+test("getStream with createStream callback content: Subsequent content requests should throw error due " +
+ "to drained content", async (t) => {
+ const resource = createBasicResource();
+ resource.getStream();
+ t.throws(() => {
+ resource.getStream();
+ }, /Content of Resource \/app\/index.html has been drained/);
+ await t.throws(resource.getBuffer(), /Content of Resource \/app\/index.html has been drained/);
+ await t.throws(resource.getString(), /Content of Resource \/app\/index.html has been drained/);
+});
+
+test("getStream with Buffer content: Subsequent content requests should throw error due to drained " +
+ "content", async (t) => {
+ const resource = createBasicResource();
+ await resource.getBuffer();
+ resource.getStream();
+ t.throws(() => {
+ resource.getStream();
+ }, /Content of Resource \/app\/index.html has been drained/);
+ await t.throws(resource.getBuffer(), /Content of Resource \/app\/index.html has been drained/);
+ await t.throws(resource.getString(), /Content of Resource \/app\/index.html has been drained/);
+});
+
+test("getStream with Stream content: Subsequent content requests should throw error due to drained " +
+ "content", async (t) => {
+ const resource = createBasicResource();
+ const {Transform} = require("stream");
+ const tStream = new Transform({
+ transform(chunk, encoding, callback) {
+ this.push(chunk.toString());
+ callback();
+ }
+ });
+ const stream = resource.getStream();
+ stream.pipe(tStream);
+ resource.setStream(tStream);
+
+ resource.getStream();
+ t.throws(() => {
+ resource.getStream();
+ }, /Content of Resource \/app\/index.html has been drained/);
+ await t.throws(resource.getBuffer(), /Content of Resource \/app\/index.html has been drained/);
+ await t.throws(resource.getString(), /Content of Resource \/app\/index.html has been drained/);
+});
+
+test("getStream with Stream content: Subsequent content requests should throw error due to drained " +
+ "content", async (t) => {
+ const resource = createBasicResource();
+ const {Transform} = require("stream");
+ const tStream = new Transform({
+ transform(chunk, encoding, callback) {
+ this.push(chunk.toString());
+ callback();
+ }
+ });
+ const stream = resource.getStream();
+ stream.pipe(tStream);
+ resource.setStream(tStream);
+
+ const p1 = resource.getBuffer();
+ const p2 = resource.getBuffer();
+
+ await t.notThrows(p1);
+
+ // Race condition in _getBufferFromStream used to cause p2
+ // to throw "Content stream of Resource /app/index.html is flagged as drained."
+ await t.notThrows(p2);
+});
diff --git a/test/lib/adapters/FileSystem.js b/test/lib/adapters/FileSystem_read.js
similarity index 75%
rename from test/lib/adapters/FileSystem.js
rename to test/lib/adapters/FileSystem_read.js
index 779573ea..b29925b3 100644
--- a/test/lib/adapters/FileSystem.js
+++ b/test/lib/adapters/FileSystem_read.js
@@ -1,6 +1,28 @@
const {test} = require("ava");
const {resourceFactory} = require("../../../");
+test("GLOB resources from application.a w/ virtual base path prefix", async (t) => {
+ const readerWriter = resourceFactory.createAdapter({
+ fsBasePath: "./test/fixtures/application.a/webapp",
+ virBasePath: "/app/"
+ });
+
+ await readerWriter.byGlob("/app/**/*.html").then(function(resources) {
+ t.deepEqual(resources.length, 1, "Found exactly one resource");
+ });
+});
+
+test("GLOB resources from application.a w/o virtual base path prefix", async (t) => {
+ const readerWriter = resourceFactory.createAdapter({
+ fsBasePath: "./test/fixtures/application.a/webapp",
+ virBasePath: "/app/"
+ });
+
+ await readerWriter.byGlob("/**/*.html").then(function(resources) {
+ t.deepEqual(resources.length, 1, "Found exactly one resource");
+ });
+});
+
test("GLOB resources from application.a w/ virtual base path prefix", async (t) => {
const readerWriter = resourceFactory.createAdapter({
fsBasePath: "./test/fixtures/application.a/webapp",
@@ -21,7 +43,6 @@ test("GLOB resources from application.a w/o virtual base path prefix", async (t)
t.deepEqual(resources.length, 1, "Found exactly one resource");
});
-
test("GLOB virtual directory w/o virtual base path prefix", async (t) => {
const readerWriter = resourceFactory.createAdapter({
fsBasePath: "./test/fixtures/application.a/webapp",
diff --git a/test/lib/adapters/FileSystem_write.js b/test/lib/adapters/FileSystem_write.js
new file mode 100644
index 00000000..a5f3a180
--- /dev/null
+++ b/test/lib/adapters/FileSystem_write.js
@@ -0,0 +1,95 @@
+const path = require("path");
+const {promisify} = require("util");
+const fs = require("fs");
+const fsAccess = promisify(fs.access);
+const {test} = require("ava");
+const rimraf = promisify(require("rimraf"));
+const chai = require("chai");
+chai.use(require("chai-fs"));
+const assert = chai.assert;
+
+const ui5Fs = require("../../../");
+
+test.beforeEach((t) => {
+ const tmpDirName = t.title.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase(); // Generate tmp dir name from test name
+
+ // Create a tmp directory for every test
+ t.context.tmpDirPath = path.join(__dirname, "..", "..", "tmp", "adapters", "FileSystemWrite", tmpDirName);
+
+ t.context.readerWriters = {
+ source: ui5Fs.resourceFactory.createAdapter({
+ fsBasePath: "./test/fixtures/application.a/webapp",
+ virBasePath: "/app/"
+ }),
+ dest: ui5Fs.resourceFactory.createAdapter({
+ fsBasePath: "./test/tmp/adapters/FileSystemWrite/" + tmpDirName,
+ virBasePath: "/app/"
+ })
+ };
+});
+
+test.afterEach.always((t) => {
+ // Cleanup tmp directory
+ return rimraf(t.context.tmpDirPath);
+});
+
+
+test("Write resource", async (t) => {
+ const readerWriters = t.context.readerWriters;
+ const destFsPath = path.join(t.context.tmpDirPath, "index.html");
+
+ // Get resource from one readerWriter
+ const resource = await readerWriters.source.byPath("/app/index.html");
+ // Write resource content to another readerWriter
+
+ await readerWriters.dest.write(resource);
+ t.notThrows(() => {
+ assert.fileEqual(destFsPath, "./test/fixtures/application.a/webapp/index.html");
+ });
+ await t.notThrows(resource.getBuffer(), "Resource content can still be accessed");
+});
+
+test("Write resource in readOnly mode", async (t) => {
+ const readerWriters = t.context.readerWriters;
+ const destFsPath = path.join(t.context.tmpDirPath, "index.html");
+
+ // Get resource from one readerWriter
+ const resource = await readerWriters.source.byPath("/app/index.html");
+ // Write resource content to another readerWriter
+ await readerWriters.dest.write(resource, {readOnly: true});
+
+ await t.notThrows(fsAccess(destFsPath, fs.constants.R_OK), "File can be read");
+ await t.throws(fsAccess(destFsPath, fs.constants.W_OK), /EACCES: permission denied|EPERM: operation not permitted/,
+ "File can not be written");
+
+ t.notThrows(() => {
+ assert.fileEqual(destFsPath, "./test/fixtures/application.a/webapp/index.html");
+ });
+ await t.notThrows(resource.getBuffer(), "Resource content can still be accessed");
+});
+
+test("Write resource in drain mode", async (t) => {
+ const readerWriters = t.context.readerWriters;
+ const destFsPath = path.join(t.context.tmpDirPath, "index.html");
+
+ // Get resource from one readerWriter
+ const resource = await readerWriters.source.byPath("/app/index.html");
+ // Write resource content to another readerWriter
+ await readerWriters.dest.write(resource, {drain: true});
+
+ t.notThrows(() => {
+ assert.fileEqual(destFsPath, "./test/fixtures/application.a/webapp/index.html");
+ });
+ return t.throws(resource.getBuffer(),
+ /Content of Resource \/app\/index.html has been drained/);
+});
+
+test("Writing with readOnly and drain options set should fail", async (t) => {
+ const readerWriters = t.context.readerWriters;
+
+ // Get resource from one readerWriter
+ const resource = await readerWriters.source.byPath("/app/index.html");
+ // Write resource content to another readerWriter
+ await t.throws(readerWriters.dest.write(resource, {readOnly: true, drain: true}),
+ "Error while writing resource /app/index.html: Do not use options 'drain' and 'readOnly' at the same time.");
+});
diff --git a/test/lib/adapters/Memory.js b/test/lib/adapters/Memory_read.js
similarity index 72%
rename from test/lib/adapters/Memory.js
rename to test/lib/adapters/Memory_read.js
index f2a5f3df..150b45cd 100644
--- a/test/lib/adapters/Memory.js
+++ b/test/lib/adapters/Memory_read.js
@@ -197,7 +197,7 @@ test("GLOB (normalized) root directory (=> fs root)", async (t) => {
await fillFromFs(readerWriter);
const resources = await readerWriter.byGlob([
- "/*/",
+ "/*",
], {nodir: false});
resources.forEach((res) => {
t.deepEqual(res._name, "app");
@@ -231,91 +231,3 @@ test("GLOB subdirectory", async (t) => {
t.deepEqual(resources[0].getPath(), "/app/application.a");
t.deepEqual(resources[0].getStatInfo().isDirectory(), true);
});
-
-test("Write resource w/ virtual base path", async (t) => {
- const readerWriter = resourceFactory.createAdapter({
- virBasePath: "/app/"
- });
-
- const res = resourceFactory.createResource({
- path: "/app/test.html"
- });
- await readerWriter.write(res);
-
- t.deepEqual(readerWriter._virFiles, {
- "test.html": res
- }, "Adapter added resource with correct path");
-
- t.deepEqual(Object.keys(readerWriter._virDirs), [], "Adapter added correct virtual directories");
-});
-
-test("Write resource w/o virtual base path", async (t) => {
- const readerWriter = resourceFactory.createAdapter({
- virBasePath: "/"
- });
-
- const res = resourceFactory.createResource({
- path: "/one/two/three/test.html"
- });
- await readerWriter.write(res);
-
- t.deepEqual(readerWriter._virFiles, {
- "one/two/three/test.html": res
- }, "Adapter added resource with correct path");
-
- t.deepEqual(Object.keys(readerWriter._virDirs), [
- "one/two/three",
- "one/two",
- "one"
- ], "Adapter added correct virtual directories");
-
- const dirRes = readerWriter._virDirs["one/two/three"];
- t.deepEqual(dirRes.getStatInfo().isDirectory(), true, "Directory resource is a directory");
- t.deepEqual(dirRes.getPath(), "/one/two/three", "Directory resource has correct path");
-});
-
-test("Write resource w/ deep virtual base path", async (t) => {
- const readerWriter = resourceFactory.createAdapter({
- virBasePath: "/app/a/"
- });
-
- const res = resourceFactory.createResource({
- path: "/app/a/one/two/three/test.html"
- });
- await readerWriter.write(res);
-
- t.deepEqual(readerWriter._virFiles, {
- "one/two/three/test.html": res
- }, "Adapter added resource with correct path");
-
- t.deepEqual(Object.keys(readerWriter._virDirs), [
- "one/two/three",
- "one/two",
- "one"
- ], "Adapter added correct virtual directories");
-
- const dirRes = readerWriter._virDirs["one/two/three"];
- t.deepEqual(dirRes.getStatInfo().isDirectory(), true, "Directory resource is a directory");
- t.deepEqual(dirRes.getPath(), "/app/a/one/two/three", "Directory resource has correct path");
-});
-
-test("Write resource w/ crazy virtual base path", async (t) => {
- const readerWriter = resourceFactory.createAdapter({
- virBasePath: "/app/🐛/"
- });
-
- const res = resourceFactory.createResource({
- path: "/app/🐛/one\\/2/3️⃣/test"
- });
- await readerWriter.write(res);
-
- t.deepEqual(readerWriter._virFiles, {
- "one\\/2/3️⃣/test": res
- }, "Adapter added resource with correct path");
-
- t.deepEqual(Object.keys(readerWriter._virDirs), [
- "one\\/2/3️⃣",
- "one\\/2",
- "one\\"
- ], "Adapter added correct virtual directories");
-});
diff --git a/test/lib/adapters/Memory_write.js b/test/lib/adapters/Memory_write.js
new file mode 100644
index 00000000..ae63652e
--- /dev/null
+++ b/test/lib/adapters/Memory_write.js
@@ -0,0 +1,120 @@
+const {test} = require("ava");
+const {resourceFactory} = require("../../../");
+
+test("GLOB resources from application.a w/ virtual base path prefix", async (t) => {
+ const dest = resourceFactory.createAdapter({
+ virBasePath: "/app/"
+ });
+
+ const res = resourceFactory.createResource({
+ path: "/app/index.html"
+ });
+ await dest.write(res)
+ .then(() => dest.byGlob("/app/*.html"))
+ .then((resources) => {
+ t.deepEqual(resources.length, 1, "Found exactly one resource");
+ });
+});
+
+test("GLOB resources from application.a w/o virtual base path prefix", async (t) => {
+ const dest = resourceFactory.createAdapter({
+ virBasePath: "/app/"
+ });
+
+ const res = resourceFactory.createResource({
+ path: "/app/index.html"
+ });
+ await dest.write(res)
+ .then(() => dest.byGlob("/**/*.html"))
+ .then((resources) => {
+ t.deepEqual(resources.length, 1, "Found exactly one resource");
+ });
+});
+
+test("Write resource w/ virtual base path", async (t) => {
+ const readerWriter = resourceFactory.createAdapter({
+ virBasePath: "/app/"
+ });
+
+ const res = resourceFactory.createResource({
+ path: "/app/test.html"
+ });
+ await readerWriter.write(res);
+
+ t.deepEqual(readerWriter._virFiles, {
+ "test.html": res
+ }, "Adapter added resource with correct path");
+
+ t.deepEqual(Object.keys(readerWriter._virDirs), [], "Adapter added correct virtual directories");
+});
+
+test("Write resource w/o virtual base path", async (t) => {
+ const readerWriter = resourceFactory.createAdapter({
+ virBasePath: "/"
+ });
+
+ const res = resourceFactory.createResource({
+ path: "/one/two/three/test.html"
+ });
+ await readerWriter.write(res);
+
+ t.deepEqual(readerWriter._virFiles, {
+ "one/two/three/test.html": res
+ }, "Adapter added resource with correct path");
+
+ t.deepEqual(Object.keys(readerWriter._virDirs), [
+ "one/two/three",
+ "one/two",
+ "one"
+ ], "Adapter added correct virtual directories");
+
+ const dirRes = readerWriter._virDirs["one/two/three"];
+ t.deepEqual(dirRes.getStatInfo().isDirectory(), true, "Directory resource is a directory");
+ t.deepEqual(dirRes.getPath(), "/one/two/three", "Directory resource has correct path");
+});
+
+test("Write resource w/ deep virtual base path", async (t) => {
+ const readerWriter = resourceFactory.createAdapter({
+ virBasePath: "/app/a/"
+ });
+
+ const res = resourceFactory.createResource({
+ path: "/app/a/one/two/three/test.html"
+ });
+ await readerWriter.write(res);
+
+ t.deepEqual(readerWriter._virFiles, {
+ "one/two/three/test.html": res
+ }, "Adapter added resource with correct path");
+
+ t.deepEqual(Object.keys(readerWriter._virDirs), [
+ "one/two/three",
+ "one/two",
+ "one"
+ ], "Adapter added correct virtual directories");
+
+ const dirRes = readerWriter._virDirs["one/two/three"];
+ t.deepEqual(dirRes.getStatInfo().isDirectory(), true, "Directory resource is a directory");
+ t.deepEqual(dirRes.getPath(), "/app/a/one/two/three", "Directory resource has correct path");
+});
+
+test("Write resource w/ crazy virtual base path", async (t) => {
+ const readerWriter = resourceFactory.createAdapter({
+ virBasePath: "/app/🐛/"
+ });
+
+ const res = resourceFactory.createResource({
+ path: "/app/🐛/one\\/2/3️⃣/test"
+ });
+ await readerWriter.write(res);
+
+ t.deepEqual(readerWriter._virFiles, {
+ "one\\/2/3️⃣/test": res
+ }, "Adapter added resource with correct path");
+
+ t.deepEqual(Object.keys(readerWriter._virDirs), [
+ "one\\/2/3️⃣",
+ "one\\/2",
+ "one\\"
+ ], "Adapter added correct virtual directories");
+});