persistence.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. // persistence.js
  2. // Helper functions for VWF node server that handles all persitence related requests
  3. var helpers = require( './helpers' ),
  4. serve = require( './serve' ),
  5. storage = require( './storagefs' ),
  6. fs = require( 'fs' ),
  7. url = require( 'url' ),
  8. libpath = require( 'path' ),
  9. querystring = require( 'querystring' );
  10. function CreateSaveDirectory( application_path, save_name, save_revision ) {
  11. var application_segments = helpers.GenerateSegments( application_path );
  12. var current_directory = "./documents";
  13. while ( application_segments.length > 0 ) {
  14. current_directory = helpers.JoinPath( current_directory, application_segments.shift() );
  15. if ( ! helpers.IsDirectory( current_directory ) ) {
  16. fs.mkdirSync( current_directory );
  17. }
  18. }
  19. current_directory = helpers.JoinPath( current_directory, save_name );
  20. if ( ! helpers.IsDirectory( current_directory ) ) {
  21. fs.mkdirSync( current_directory );
  22. }
  23. }
  24. // LoadSaveObject function is designed to take the load information provided by the
  25. // GetLoadInformation function, load and parse the specified file, and return that object
  26. // (or undefined, if there is no such file ).
  27. function LoadSaveObject( loadInfo ) {
  28. if ( loadInfo[ 'save_name' ] ) {
  29. var fileName = helpers.JoinPath( "./documents", loadInfo[ 'application_path' ], loadInfo[ 'save_name' ], "saveState_" + loadInfo[ 'save_revision' ] + ".vwf.json" );
  30. if ( helpers.IsFile( fileName ) ) {
  31. var fileContents = fs.readFileSync( fileName, "utf8" );
  32. return JSON.parse( fileContents );
  33. }
  34. }
  35. return undefined;
  36. }
  37. // LookupSaveRevisions takes the public path and the name of a save, and provides
  38. // an array of all revisions for that save. (If the save does not exist, this will be
  39. // an empty array).
  40. function LookupSaveRevisions( public_path, save_name ) {
  41. var result = [ ];
  42. var directoryName = helpers.JoinPath( "./documents/", public_path, save_name );
  43. if ( helpers.IsDirectory( directoryName ) ) {
  44. var potentialSaves = fs.readdirSync( directoryName.replace( /\//g, libpath.sep ) );
  45. for ( var index = 0; index < potentialSaves.length; index++ ) {
  46. if ( potentialSaves[ index ].match( /^saveState_\d+\.vwf\.json$/ ) ) {
  47. result.push( parseInt( potentialSaves[ index ].slice( 10, potentialSaves[ index ].length - 9 ) ) );
  48. }
  49. }
  50. }
  51. return result;
  52. }
  53. // GetLoadInformation receives a parsed request {private_path, public_path, instance, application} and returns the
  54. // details of the save that is designated by the initial request. The details are returned in an object
  55. // composed of: save_name (name of the save) save_revision (revision of the save), explicit_revision (boolean, true if the request
  56. // explicitly specified the revision, false if it did not), and application_path (the public_path of the application this is a save for).
  57. function GetLoadInformation( parsedRequest ) {
  58. var result = {'save_name': undefined, 'save_revision': undefined, 'explicit_revision': undefined, 'application_path': undefined };
  59. if ( parsedRequest[ 'private_path' ] ) {
  60. var segments = helpers.GenerateSegments( parsedRequest[ 'private_path' ] );
  61. if ( ( segments.length > 1 ) && ( segments[ 0 ] == "load" ) ) {
  62. var potentialRevisions = LookupSaveRevisions( parsedRequest[ 'public_path' ], segments[ 1 ] );
  63. if ( potentialRevisions.length > 0 ) {
  64. result[ 'save_name' ] = segments[ 1 ];
  65. if ( segments.length > 2 ) {
  66. var requestedRevision = parseInt( segments[ 2 ] );
  67. if ( requestedRevision ) {
  68. if ( potentialRevisions.indexOf( requestedRevision ) > -1 ) {
  69. result[ 'save_revision' ] = requestedRevision;
  70. result[ 'explicit_revision' ] = true;
  71. result[ 'application_path' ] = parsedRequest[ 'public_path' ];
  72. }
  73. }
  74. }
  75. if ( result[ 'explicit_revision' ] == undefined ) {
  76. result[ 'explicit_revision' ] = false;
  77. potentialRevisions.sort( );
  78. result[ 'save_revision' ] = potentialRevisions.pop();
  79. result[ 'application_path' ] = parsedRequest[ 'public_path' ];
  80. }
  81. }
  82. }
  83. }
  84. return result;
  85. }
  86. // GetSaveInformation is a helper function that takes a NodeJS request, a NodeJS response,
  87. // and the application_path (/path/to/application). It returns an array of all saves found for that
  88. // application (including separate entries for individual revisions of saves ).
  89. function GetSaveInformation( request, response, application_path ) {
  90. var result = [ ];
  91. var potentialSaveNames = fs.readdirSync( helpers.JoinPath("./documents", application_path ) );
  92. for ( var index = 0; index < potentialSaveNames.length; index++ ) {
  93. if ( helpers.IsDirectory( helpers.JoinPath( "./documents", application_path, potentialSaveNames[ index ] ) ) ) {
  94. var revisionList = LookupSaveRevisions( application_path, potentialSaveNames[ index ] );
  95. var latestsave = true;
  96. revisionList.sort();
  97. while ( revisionList.length > 0 ) {
  98. var newEntry = {};
  99. newEntry[ 'applicationpath' ] = application_path;
  100. newEntry[ 'savename' ] = potentialSaveNames[ index ];
  101. newEntry[ 'revision' ] = revisionList.pop().toString();
  102. newEntry[ 'latestsave' ] = latestsave;
  103. if ( latestsave ) {
  104. newEntry[ 'url' ] = helpers.JoinPath( "http://" + request.headers.host, application_path, "load", potentialSaveNames[ index ] + "/" );
  105. }
  106. else {
  107. newEntry[ 'url' ] = helpers.JoinPath( "http://" + request.headers.host, application_path, "load", potentialSaveNames[ index ] + "/", newEntry[ 'revision' ] + "/" );
  108. }
  109. latestsave = false;
  110. result.push( newEntry );
  111. }
  112. }
  113. }
  114. return result;
  115. }
  116. // GetSaveInformationRecursive is a helper function that recursively calls the
  117. // GetSaveInformation function on a directory, and all directories within it (and within those, and so on).
  118. function GetSaveInformationRecursive( request, response, application_path ) {
  119. var result = [];
  120. var subDirectories = fs.readdirSync( helpers.JoinPath("./documents", application_path ) );
  121. for ( var index = 0; index < subDirectories.length; index++ ) {
  122. if ( helpers.IsDirectory( helpers.JoinPath( "./documents", application_path, subDirectories[ index ] ) ) ) {
  123. result = result.concat( GetSaveInformationRecursive( request, response, helpers.JoinPath( application_path, subDirectories[ index ] ) ) );
  124. }
  125. }
  126. result = result.concat( GetSaveInformation( request, response, application_path ) );
  127. return result;
  128. }
  129. // HandlePersistenceLoad attempts to deal with a request which contains load arguments in the
  130. // private_path.
  131. // If the URL is properly formed, it will either (if the load portion of the URL is the last
  132. // portion of the URL, and there is no trailing slash) redirect to the same URL with a trailing
  133. // slash added, or if the URL is otherwise properly formed, it will remove the load specific
  134. // portions of the private_path, and pass this updated parsedRequest on to the normal application
  135. // level Serve function (effectively, load URL's are treated as if all load specific portions
  136. // of the URL don't exist, except for the websocket connection, which uses it to load the initial
  137. // state for the first client. ).
  138. function HandlePersistenceLoad( request, response, parsedRequest, segments ) {
  139. var loadInformation = GetLoadInformation( parsedRequest );
  140. if ( loadInformation[ 'save_name' ] != undefined ) {
  141. segments.shift();
  142. segments.shift();
  143. if ( loadInformation[ 'explicit_revision' ] ) {
  144. segments.shift();
  145. }
  146. var subParsedRequest = { };
  147. subParsedRequest[ 'public_path' ] = parsedRequest[ 'public_path' ];
  148. subParsedRequest[ 'application' ] = parsedRequest[ 'application' ];
  149. if ( segments.length > 0 ) {
  150. subParsedRequest[ 'private_path' ] = segments.join("/");
  151. }
  152. else {
  153. subParsedRequest[ 'private_path' ] = undefined;
  154. }
  155. subParsedRequest[ 'instance' ] = parsedRequest[ 'instance' ];
  156. if ( subParsedRequest[ 'instance' ] ) {
  157. return global.application.Serve( request, response, subParsedRequest );
  158. }
  159. else if ( request.method == "GET" ) {
  160. var redirectPath = parsedRequest[ 'public_path' ];
  161. if ( parsedRequest[ 'application' ] ) {
  162. redirectPath = helpers.JoinPath( redirectPath, parsedRequest[ 'application' ] );
  163. }
  164. redirectPath = helpers.JoinPath( redirectPath, helpers.GenerateInstanceID( ), parsedRequest[ 'private_path' ] );
  165. if ( segments.length == 0 ) {
  166. redirectPath = helpers.JoinPath( redirectPath, "/");
  167. }
  168. serve.Redirect( redirectPath, response );
  169. return true;
  170. }
  171. }
  172. return false;
  173. }
  174. function GenerateSaveObject( request, public_path, application, instance, save_name ) {
  175. var result = {};
  176. result[ "name" ] = save_name;
  177. result[ "url" ] = helpers.JoinPath( "http://" + request.headers.host, public_path, application, instance, "saves", save_name );
  178. result[ "vwf_info" ] = {};
  179. result[ "vwf_info" ][ "public_path" ] = public_path;
  180. result[ "vwf_info" ][ "application" ] = application;
  181. result[ "vwf_info" ][ "path_to_application" ] = helpers.JoinPath( public_path, application );
  182. result[ "vwf_info" ][ "instance" ] = instance;
  183. var metadata = storage.GetSaveMetadata( public_path, application, instance, save_name );
  184. if ( metadata ) {
  185. result[ "metadata" ] = metadata;
  186. }
  187. else {
  188. result[ "metadata" ] = {}
  189. }
  190. return result;
  191. }
  192. function HandleSavesLoad( request, response, parsedRequest, segments ) {
  193. // Saves are listed/loaded via GET, and saved via POST
  194. if ( request.method == "GET" ) {
  195. //Dealing with either loading a save state, or listing save states,
  196. // or a malformed request.
  197. if ( segments.length == 1 ) {
  198. //No arguments beyond saves, must be listing save states.
  199. // If instance was set, listing saves for that instance,
  200. // otherwise listing saves for all instances of this application.
  201. if ( parsedRequest[ 'instance' ] ) {
  202. //Listing for specific instance.
  203. var save_name_list = storage.ListInstanceSaveStates( parsedRequest[ 'public_path' ], parsedRequest[ 'application' ], parsedRequest[ 'instance' ] );
  204. var save_objects = [ ];
  205. for ( var index = 0; index < save_name_list.length; index++ ) {
  206. save_objects.push( GenerateSaveObject( request, parsedRequest[ 'public_path' ], parsedRequest[ 'application' ], parsedRequest[ 'instance' ], save_name_list[ index ] ) );
  207. }
  208. serve.JSON( save_objects, response, url.parse( request.url, true ) );
  209. return true;
  210. }
  211. else {
  212. var save_hash = storage.ListApplicationSaveStates( parsedRequest[ 'public_path' ], parsedRequest[ 'application' ] );
  213. var instance_hash = { };
  214. for ( instance_id in save_hash ) {
  215. var save_objects = [ ];
  216. for ( var index = 0; index < save_hash[ instance_id ].length; index++ ) {
  217. save_objects.push( GenerateSaveObject( request, parsedRequest[ 'public_path' ], parsedRequest[ 'application' ], instance_id, save_hash[ instance_id ][ index ] ) );
  218. }
  219. instance_hash[ instance_id ] = save_objects;
  220. }
  221. serve.JSON( instance_hash, response, url.parse( request.url, true ) );
  222. return true;
  223. }
  224. } else if ( segements.length == 2 ) {
  225. }
  226. } else if ( request.method == "POST" ) {
  227. }
  228. return false;
  229. }
  230. function HandlePersistenceSave( request, response, parsedRequest, segments ) {
  231. if ( segments.length == 2 ) {
  232. var saveName = segments[ 1 ];
  233. var saveRevision = new Date().valueOf();
  234. CreateSaveDirectory( parsedRequest[ 'public_path' ], saveName, saveRevision );
  235. var persistenceData = "";
  236. var persistenceLength = 0;
  237. request.on( 'data', function ( data ) {
  238. persistenceData += data;
  239. if ( persistenceData.length > 50000000 ) {
  240. persistenceData = "";
  241. response.writeHead( 413, { 'Content-Type': 'text/plain' } ).end( );
  242. request.connection.destroy();
  243. }
  244. } );
  245. request.on( 'end', function( ) {
  246. if ( persistenceData.length > 0 ) {
  247. var processedPost = querystring.parse( persistenceData );
  248. fs.writeFileSync( helpers.JoinPath( './documents', parsedRequest[ 'public_path' ], saveName, 'saveState' + processedPost[ "extension" ] ), processedPost[ "jsonState" ] );
  249. fs.writeFileSync( helpers.JoinPath( './documents', parsedRequest[ 'public_path' ], saveName, 'saveState_' + saveRevision + processedPost[ "extension" ] ), processedPost[ "jsonState" ] );
  250. }
  251. } );
  252. return true;
  253. }
  254. return false;
  255. }
  256. // The Serve function takes the nodeJS request, nodeJS response and the parsedRequest, and
  257. // attempts to see if it is a properly formed persistence related request.
  258. // If so, it serves the request, and returns true.
  259. // If it is an incorrectly formed persistence request, or not a persistence request
  260. // at all, then false will be returned.
  261. function Serve( request, response, parsedRequest ) {
  262. if ( parsedRequest[ 'private_path' ] ) {
  263. var segments = helpers.GenerateSegments( parsedRequest[ 'private_path' ] );
  264. if ( segments.length > 0 ) {
  265. switch ( segments[ 0 ] ) {
  266. case "listallsaves":
  267. if ( request.method == "GET" ) {
  268. var saveInfo = GetSaveInformationRecursive( request, response, "/" );
  269. serve.JSON( saveInfo, response, url.parse( request.url, true ) );
  270. return true;
  271. }
  272. return false;
  273. case "listsaves":
  274. if ( request.method == "GET" ) {
  275. var saveInfo = GetSaveInformation( request, response, parsedRequest[ 'public_path' ] );
  276. serve.JSON( saveInfo, response, url.parse( request.url, true ) );
  277. return true;
  278. }
  279. return false;
  280. case "listdescendentsaves":
  281. if ( request.method == "GET" ) {
  282. var saveInfo = GetSaveInformationRecursive( request, response, parsedRequest[ 'public_path' ] );
  283. serve.JSON( saveInfo, response, url.parse( request.url, true ) );
  284. return true;
  285. }
  286. return false;
  287. case "load":
  288. return HandlePersistenceLoad( request, response, parsedRequest, segments );
  289. case "saves":
  290. return HandleSavesLoad( request, response, parsedRequest, segments );
  291. case "save":
  292. if ( ( request.method == "POST" ) && ( segments.length > 1 ) ) {
  293. return HandlePersistenceSave( request, response, parsedRequest, segments );
  294. }
  295. }
  296. }
  297. }
  298. return false;
  299. }
  300. exports.GetLoadInformation = GetLoadInformation;
  301. exports.Serve = Serve;
  302. exports.LoadSaveObject = LoadSaveObject;