fs.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. 'use strict';
  2. 'require rpc';
  3. 'require request';
  4. 'require baseclass';
  5. /**
  6. * @typedef {Object} FileStatEntry
  7. * @memberof LuCI.fs
  8. * @property {string} name - Name of the directory entry
  9. * @property {string} type - Type of the entry, one of `block`, `char`, `directory`, `fifo`, `symlink`, `file`, `socket` or `unknown`
  10. * @property {number} size - Size in bytes
  11. * @property {number} mode - Access permissions
  12. * @property {number} atime - Last access time in seconds since epoch
  13. * @property {number} mtime - Last modification time in seconds since epoch
  14. * @property {number} ctime - Last change time in seconds since epoch
  15. * @property {number} inode - Inode number
  16. * @property {number} uid - Numeric owner id
  17. * @property {number} gid - Numeric group id
  18. */
  19. /**
  20. * @typedef {Object} FileExecResult
  21. * @memberof LuCI.fs
  22. *
  23. * @property {number} code - The exit code of the invoked command
  24. * @property {string} [stdout] - The stdout produced by the command, if any
  25. * @property {string} [stderr] - The stderr produced by the command, if any
  26. */
  27. var callFileList, callFileStat, callFileRead, callFileWrite, callFileRemove,
  28. callFileExec, callFileMD5;
  29. callFileList = rpc.declare({
  30. object: 'file',
  31. method: 'list',
  32. params: [ 'path' ]
  33. });
  34. callFileStat = rpc.declare({
  35. object: 'file',
  36. method: 'stat',
  37. params: [ 'path' ]
  38. });
  39. callFileRead = rpc.declare({
  40. object: 'file',
  41. method: 'read',
  42. params: [ 'path' ]
  43. });
  44. callFileWrite = rpc.declare({
  45. object: 'file',
  46. method: 'write',
  47. params: [ 'path', 'data', 'mode' ]
  48. });
  49. callFileRemove = rpc.declare({
  50. object: 'file',
  51. method: 'remove',
  52. params: [ 'path' ]
  53. });
  54. callFileExec = rpc.declare({
  55. object: 'file',
  56. method: 'exec',
  57. params: [ 'command', 'params', 'env' ]
  58. });
  59. callFileMD5 = rpc.declare({
  60. object: 'file',
  61. method: 'md5',
  62. params: [ 'path' ]
  63. });
  64. var rpcErrors = [
  65. null,
  66. 'InvalidCommandError',
  67. 'InvalidArgumentError',
  68. 'MethodNotFoundError',
  69. 'NotFoundError',
  70. 'NoDataError',
  71. 'PermissionError',
  72. 'TimeoutError',
  73. 'UnsupportedError'
  74. ];
  75. function handleRpcReply(expect, rc) {
  76. if (typeof(rc) == 'number' && rc != 0) {
  77. var e = new Error(rpc.getStatusText(rc)); e.name = rpcErrors[rc] || 'Error';
  78. throw e;
  79. }
  80. if (expect) {
  81. var type = Object.prototype.toString;
  82. for (var key in expect) {
  83. if (rc != null && key != '')
  84. rc = rc[key];
  85. if (rc == null || type.call(rc) != type.call(expect[key])) {
  86. var e = new Error(_('Unexpected reply data format')); e.name = 'TypeError';
  87. throw e;
  88. }
  89. break;
  90. }
  91. }
  92. return rc;
  93. }
  94. function handleCgiIoReply(res) {
  95. if (!res.ok || res.status != 200) {
  96. var e = new Error(res.statusText);
  97. switch (res.status) {
  98. case 400:
  99. e.name = 'InvalidArgumentError';
  100. break;
  101. case 403:
  102. e.name = 'PermissionError';
  103. break;
  104. case 404:
  105. e.name = 'NotFoundError';
  106. break;
  107. default:
  108. e.name = 'Error';
  109. }
  110. throw e;
  111. }
  112. switch (this.type) {
  113. case 'blob':
  114. return res.blob();
  115. case 'json':
  116. return res.json();
  117. default:
  118. return res.text();
  119. }
  120. }
  121. /**
  122. * @class fs
  123. * @memberof LuCI
  124. * @hideconstructor
  125. * @classdesc
  126. *
  127. * Provides high level utilities to wrap file system related RPC calls.
  128. * To import the class in views, use `'require fs'`, to import it in
  129. * external JavaScript, use `L.require("fs").then(...)`.
  130. */
  131. var FileSystem = baseclass.extend(/** @lends LuCI.fs.prototype */ {
  132. /**
  133. * Obtains a listing of the specified directory.
  134. *
  135. * @param {string} path
  136. * The directory path to list.
  137. *
  138. * @returns {Promise<LuCI.fs.FileStatEntry[]>}
  139. * Returns a promise resolving to an array of stat detail objects or
  140. * rejecting with an error stating the failure reason.
  141. */
  142. list: function(path) {
  143. return callFileList(path).then(handleRpcReply.bind(this, { entries: [] }));
  144. },
  145. /**
  146. * Return file stat information on the specified path.
  147. *
  148. * @param {string} path
  149. * The filesystem path to stat.
  150. *
  151. * @returns {Promise<LuCI.fs.FileStatEntry>}
  152. * Returns a promise resolving to a stat detail object or
  153. * rejecting with an error stating the failure reason.
  154. */
  155. stat: function(path) {
  156. return callFileStat(path).then(handleRpcReply.bind(this, { '': {} }));
  157. },
  158. /**
  159. * Read the contents of the given file and return them.
  160. * Note: this function is unsuitable for obtaining binary data.
  161. *
  162. * @param {string} path
  163. * The file path to read.
  164. *
  165. * @returns {Promise<string>}
  166. * Returns a promise resolving to a string containing the file contents or
  167. * rejecting with an error stating the failure reason.
  168. */
  169. read: function(path) {
  170. return callFileRead(path).then(handleRpcReply.bind(this, { data: '' }));
  171. },
  172. /**
  173. * Write the given data to the specified file path.
  174. * If the specified file path does not exist, it will be created, given
  175. * sufficient permissions.
  176. *
  177. * Note: `data` will be converted to a string using `String(data)` or to
  178. * `''` when it is `null`.
  179. *
  180. * @param {string} path
  181. * The file path to write to.
  182. *
  183. * @param {*} [data]
  184. * The file data to write. If it is null, it will be set to an empty
  185. * string.
  186. *
  187. * @param {number} [mode]
  188. * The permissions to use on file creation. Default is 420 (0644).
  189. *
  190. * @returns {Promise<number>}
  191. * Returns a promise resolving to `0` or rejecting with an error stating
  192. * the failure reason.
  193. */
  194. write: function(path, data, mode) {
  195. data = (data != null) ? String(data) : '';
  196. mode = (mode != null) ? mode : 420; // 0644
  197. return callFileWrite(path, data, mode).then(handleRpcReply.bind(this, { '': 0 }));
  198. },
  199. /**
  200. * Unlink the given file.
  201. *
  202. * @param {string}
  203. * The file path to remove.
  204. *
  205. * @returns {Promise<number>}
  206. * Returns a promise resolving to `0` or rejecting with an error stating
  207. * the failure reason.
  208. */
  209. remove: function(path) {
  210. return callFileRemove(path).then(handleRpcReply.bind(this, { '': 0 }));
  211. },
  212. /**
  213. * Execute the specified command, optionally passing params and
  214. * environment variables.
  215. *
  216. * Note: The `command` must be either the path to an executable,
  217. * or a basename without arguments in which case it will be searched
  218. * in $PATH. If specified, the values given in `params` will be passed
  219. * as arguments to the command.
  220. *
  221. * The key/value pairs in the optional `env` table are translated to
  222. * `setenv()` calls prior to running the command.
  223. *
  224. * @param {string} command
  225. * The command to invoke.
  226. *
  227. * @param {string[]} [params]
  228. * The arguments to pass to the command.
  229. *
  230. * @param {Object.<string, string>} [env]
  231. * Environment variables to set.
  232. *
  233. * @returns {Promise<LuCI.fs.FileExecResult>}
  234. * Returns a promise resolving to an object describing the execution
  235. * results or rejecting with an error stating the failure reason.
  236. */
  237. exec: function(command, params, env) {
  238. if (!Array.isArray(params))
  239. params = null;
  240. if (!L.isObject(env))
  241. env = null;
  242. return callFileExec(command, params, env).then(handleRpcReply.bind(this, { '': {} }));
  243. },
  244. /**
  245. * Read the contents of the given file, trim leading and trailing white
  246. * space and return the trimmed result. In case of errors, return an empty
  247. * string instead.
  248. *
  249. * Note: this function is useful to read single-value files in `/sys`
  250. * or `/proc`.
  251. *
  252. * This function is guaranteed to not reject its promises, on failure,
  253. * an empty string will be returned.
  254. *
  255. * @param {string} path
  256. * The file path to read.
  257. *
  258. * @returns {Promise<string>}
  259. * Returns a promise resolving to the file contents or the empty string
  260. * on failure.
  261. */
  262. trimmed: function(path) {
  263. return L.resolveDefault(this.read(path), '').then(function(s) {
  264. return s.trim();
  265. });
  266. },
  267. /**
  268. * Read the contents of the given file, split it into lines, trim
  269. * leading and trailing white space of each line and return the
  270. * resulting array.
  271. *
  272. * This function is guaranteed to not reject its promises, on failure,
  273. * an empty array will be returned.
  274. *
  275. * @param {string} path
  276. * The file path to read.
  277. *
  278. * @returns {Promise<string[]>}
  279. * Returns a promise resolving to an array containing the stripped lines
  280. * of the given file or `[]` on failure.
  281. */
  282. lines: function(path) {
  283. return L.resolveDefault(this.read(path), '').then(function(s) {
  284. var lines = [];
  285. s = s.trim();
  286. if (s != '') {
  287. var l = s.split(/\n/);
  288. for (var i = 0; i < l.length; i++)
  289. lines.push(l[i].trim());
  290. }
  291. return lines;
  292. });
  293. },
  294. /**
  295. * Read the contents of the given file and return them, bypassing ubus.
  296. *
  297. * This function will read the requested file through the cgi-io
  298. * helper applet at `/cgi-bin/cgi-download` which bypasses the ubus rpc
  299. * transport. This is useful to fetch large file contents which might
  300. * exceed the ubus message size limits or which contain binary data.
  301. *
  302. * The cgi-io helper will enforce the same access permission rules as
  303. * the ubus based read call.
  304. *
  305. * @param {string} path
  306. * The file path to read.
  307. *
  308. * @param {string} [type=text]
  309. * The expected type of read file contents. Valid values are `text` to
  310. * interpret the contents as string, `json` to parse the contents as JSON
  311. * or `blob` to return the contents as Blob instance.
  312. *
  313. * @returns {Promise<*>}
  314. * Returns a promise resolving with the file contents interpreted according
  315. * to the specified type or rejecting with an error stating the failure
  316. * reason.
  317. */
  318. read_direct: function(path, type) {
  319. var postdata = 'sessionid=%s&path=%s'
  320. .format(encodeURIComponent(L.env.sessionid), encodeURIComponent(path));
  321. return request.post(L.env.cgi_base + '/cgi-download', postdata, {
  322. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  323. responseType: (type == 'blob') ? 'blob' : 'text'
  324. }).then(handleCgiIoReply.bind({ type: type }));
  325. },
  326. /**
  327. * Execute the specified command, bypassing ubus.
  328. *
  329. * Note: The `command` must be either the path to an executable,
  330. * or a basename without arguments in which case it will be searched
  331. * in $PATH. If specified, the values given in `params` will be passed
  332. * as arguments to the command.
  333. *
  334. * This function will invoke the requested commands through the cgi-io
  335. * helper applet at `/cgi-bin/cgi-exec` which bypasses the ubus rpc
  336. * transport. This is useful to fetch large command outputs which might
  337. * exceed the ubus message size limits or which contain binary data.
  338. *
  339. * The cgi-io helper will enforce the same access permission rules as
  340. * the ubus based exec call.
  341. *
  342. * @param {string} command
  343. * The command to invoke.
  344. *
  345. * @param {string[]} [params]
  346. * The arguments to pass to the command.
  347. *
  348. * @param {string} [type=text]
  349. * The expected output type of the invoked program. Valid values are
  350. * `text` to interpret the output as string, `json` to parse the output
  351. * as JSON or `blob` to return the output as Blob instance.
  352. *
  353. * @param {boolean} [latin1=false]
  354. * Whether to encode the command line as Latin1 instead of UTF-8. This
  355. * is usually not needed but can be useful for programs that cannot
  356. * handle UTF-8 input.
  357. *
  358. * @returns {Promise<*>}
  359. * Returns a promise resolving with the command stdout output interpreted
  360. * according to the specified type or rejecting with an error stating the
  361. * failure reason.
  362. */
  363. exec_direct: function(command, params, type, latin1) {
  364. var cmdstr = String(command)
  365. .replace(/\\/g, '\\\\').replace(/(\s)/g, '\\$1');
  366. if (Array.isArray(params))
  367. for (var i = 0; i < params.length; i++)
  368. cmdstr += ' ' + String(params[i])
  369. .replace(/\\/g, '\\\\').replace(/(\s)/g, '\\$1');
  370. if (latin1)
  371. cmdstr = escape(cmdstr).replace(/\+/g, '%2b');
  372. else
  373. cmdstr = encodeURIComponent(cmdstr);
  374. var postdata = 'sessionid=%s&command=%s'
  375. .format(encodeURIComponent(L.env.sessionid), cmdstr);
  376. return request.post(L.env.cgi_base + '/cgi-exec', postdata, {
  377. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  378. responseType: (type == 'blob') ? 'blob' : 'text'
  379. }).then(handleCgiIoReply.bind({ type: type }));
  380. }
  381. });
  382. return FileSystem;