IF YOU WOULD LIKE TO GET AN ACCOUNT, please write an email to s dot adaszewski at gmail dot com. User accounts are meant only to report issues and/or generate pull requests. This is a purpose-specific Git hosting for ADARED projects. Thank you for your understanding!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

252 lines
8.8KB

  1. //
  2. // Copyright (C) Stanislaw Adaszewski, 2020
  3. // Contact: s.adaszewski@gmail.com
  4. // Website: https://adared.ch/wba
  5. // License: GNU Affero General Public License, Version 3
  6. //
  7. import { h, Component } from 'preact';
  8. import WBTable from 'wb-table';
  9. import WBBreadcrumbs from 'wb-breadcrumbs';
  10. import WBPagination from 'wb-pagination';
  11. import makeArvadosRequest from 'make-arvados-request';
  12. import wbDownloadFile from 'wb-download-file';
  13. import WBManifestWorkerWrapper from 'wb-manifest-worker-wrapper';
  14. function unescapeName(name) {
  15. return name.replace(/(\\\\|\\[0-9]{3})/g,
  16. (_, $1) => ($1 === '\\\\' ? '\\' : String.fromCharCode(parseInt($1.substr(1), 8))));
  17. }
  18. function encodeURIComponentIncludingDots(s) {
  19. return encodeURIComponent(s).replace('.', '%2E');
  20. }
  21. function endsWith(what, endings) {
  22. if (typeof(endings) === 'string')
  23. return what.endsWith(endings);
  24. if (endings instanceof Array)
  25. return endings.map(a => what.endsWith(a)).reduce((a, b) => (a || b));
  26. throw Error('Expected second argument to be either a string or an array');
  27. }
  28. function maskRows(rows) {
  29. return rows.map(r => r.map(c => '-'));
  30. }
  31. class WBCollectionContent extends Component {
  32. constructor(...args) {
  33. super(...args);
  34. this.state.rows = [];
  35. this.state.manifestWorker = new WBManifestWorkerWrapper();
  36. this.state.loaded = 0;
  37. this.state.total = 0;
  38. this.state.mode = 'manifestDownload';
  39. this.state.parsedStreams = 0;
  40. this.state.totalStreams = 1;
  41. }
  42. getUrl(params) {
  43. let res = '/collection-browse/' +
  44. ('uuid' in params ? params.uuid : this.props.uuid) + '/' +
  45. encodeURIComponentIncludingDots('collectionPath' in params ? params.collectionPath : this.props.collectionPath) + '/' +
  46. ('page' in params ? params.page : this.props.page);
  47. return res;
  48. }
  49. componentDidMount() {
  50. let { arvHost, arvToken } = this.props.app.state;
  51. let { uuid, collectionPath } = this.props;
  52. let { manifestWorker } = this.state;
  53. let select = [ 'manifest_text' ];
  54. let prom = makeArvadosRequest(arvHost, arvToken,
  55. '/arvados/v1/collections/' + uuid +
  56. '?select=' + encodeURIComponent(JSON.stringify(select)),
  57. { 'onProgress': e => {
  58. this.setState({ 'loaded': e.loaded, 'total': e.total });
  59. } });
  60. prom = prom.then(xhr => {
  61. const streams = xhr.response.manifest_text.split('\n');
  62. const paths = streams.filter(s => s).map(s => {
  63. const n = s.indexOf(' ');
  64. return unescapeName(s.substr(0, n));
  65. });
  66. let prom_1 = new Promise(accept => accept());
  67. prom_1 = prom_1.then(() => {
  68. this.setState({
  69. 'totalStreams': streams.length,
  70. 'parsedStreams': 0,
  71. 'mode': 'manifestParse'
  72. });
  73. return manifestWorker.postMessage([ 'precreatePaths', paths ]);
  74. });
  75. let lastListingTimestamp = new Date(0);
  76. for (let i = 0; i < streams.length; i++) {
  77. prom_1 = prom_1.then(() => manifestWorker.postMessage([ 'parseStream', streams[i] ]));
  78. prom_1 = prom_1.then(() => {
  79. if (new Date() - lastListingTimestamp < 1000)
  80. return;
  81. lastListingTimestamp = new Date();
  82. let prom_2 = new Promise(accept => accept());
  83. prom_2 = prom_2.then(() => manifestWorker.postMessage([
  84. 'listDirectory', '.' + this.props.collectionPath, true
  85. ]));
  86. prom_2 = prom_2.then(e => {
  87. this.prepareRows(e.data[1]);
  88. this.setState({ 'parsedStreams': (i + 1) });
  89. });
  90. return prom_2;
  91. });
  92. }
  93. prom_1 = prom_1.then(() => manifestWorker.postMessage([ 'listDirectory',
  94. '.' + this.props.collectionPath, true ]));
  95. prom_1 = prom_1.then(e => {
  96. this.state.mode = 'browsingReady';
  97. this.prepareRows(e.data[1]);
  98. });
  99. return prom_1;
  100. });
  101. }
  102. componentWillReceiveProps(nextProps) {
  103. this.props = nextProps;
  104. this.setState({ rows: maskRows(this.state.rows) });
  105. const { manifestWorker, mode } = this.state;
  106. const { collectionPath } = this.props;
  107. if (mode === 'browsingReady') {
  108. let prom = manifestWorker.postMessage([ 'listDirectory', '.' + collectionPath, true ]);
  109. prom = prom.then(e => this.prepareRows(e.data[1]));
  110. }
  111. }
  112. prepareRows(listing) {
  113. let { manifestWorker, mode } = this.state;
  114. let { collectionPath, page, itemsPerPage, app } = this.props;
  115. let { arvHost, arvToken } = app.state;
  116. const numPages = Math.ceil(listing.length / itemsPerPage);
  117. listing = listing.slice(page * itemsPerPage,
  118. page * itemsPerPage + itemsPerPage);
  119. this.setState({
  120. 'numPages': numPages,
  121. 'rows': listing.map(item => (
  122. (item[0] === 'd') ? [
  123. (<a href={ this.getUrl({ 'collectionPath': collectionPath + '/' + item[1], 'page': 0 }) }>{ item[1] }/</a>),
  124. 'Directory',
  125. null,
  126. (<div></div>)
  127. ] : [
  128. item[1],
  129. 'File',
  130. filesize(item[2]),
  131. ( (mode === 'browsingReady') ? (
  132. <div>
  133. <button class="btn btn-outline-primary mx-1" title="Download"
  134. onclick={ () => manifestWorker.postMessage([ 'getFile',
  135. '.' + collectionPath + '/' + item[1] ]).then(e => {
  136. const file = e.data[1];
  137. const blob = new Blob([
  138. JSON.stringify([ arvHost, arvToken, item[1], file ])
  139. ]);
  140. const blocksBlobUrl = URL.createObjectURL(blob);
  141. window.open('/download/' + encodeURIComponent(blocksBlobUrl), '_blank');
  142. }) }><i class="fas fa-download"></i></button>
  143. <button class="btn btn-outline-primary mx-1" title="View"
  144. onclick={ () => {
  145. alert('Not implemented.')
  146. } }><i class="far fa-eye"></i></button>
  147. { endsWith(item[1].toLowerCase(), ['.nii', '.nii.gz']) ? (
  148. <button class="btn btn-outline-primary mx-1" title="View Image"
  149. onclick={ () => manifestWorker.postMessage([ 'getFile',
  150. '.' + collectionPath + '/' + item[1] ]).then(e => {
  151. const file = e.data[1];
  152. const blob = new Blob([
  153. JSON.stringify({ 'name': item[1], 'file': file })
  154. ]);
  155. const blocksBlobUrl = URL.createObjectURL(blob);
  156. window.open('/image-viewer/' + encodeURIComponent(blocksBlobUrl), '_blank');
  157. }) }><i class="fas fa-image"></i></button>
  158. ) : null }
  159. </div>
  160. ) : null)
  161. ]
  162. ))
  163. });
  164. }
  165. render({ collectionPath, page }, { manifestReader, rows,
  166. numPages, loaded, total, mode, parsedStreams, totalStreams }) {
  167. return (
  168. <div>
  169. <WBBreadcrumbs items={ ('.' + collectionPath).split('/').map((name, index) => ({ name, index })) }
  170. getItemUrl={ it => this.getUrl({
  171. collectionPath: ('.' + collectionPath).split('/').slice(0, it.index + 1).join('/').substr(1),
  172. page: 0
  173. }) } />
  174. { (mode === 'manifestDownload') ?
  175. (
  176. <div class="container-fluid">
  177. <div>Downloading manifest: { filesize(loaded) }</div>
  178. <div class="progress">
  179. <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar"
  180. aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%"></div>
  181. </div>
  182. </div>
  183. ) : (
  184. <div>
  185. { mode === 'manifestParse' ? (
  186. <div class="container-fluid mb-2">
  187. <div>Parsing manifest: { parsedStreams }/{ totalStreams }</div>
  188. <div class="progress">
  189. <div class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar"
  190. aria-valuenow={ totalStreams } aria-valuemin="0" aria-valuemax={ parsedStreams } style={ 'width: ' + Math.round(parsedStreams * 100 / totalStreams) + '%' }></div>
  191. </div>
  192. </div>
  193. ) : null }
  194. <WBTable columns={ [ 'Name', 'Type', 'Size', 'Actions' ] }
  195. rows={ rows } />
  196. <WBPagination activePage={ page } numPages={ numPages }
  197. getPageUrl={ page => this.getUrl({ 'page': page }) } />
  198. </div>
  199. ) }
  200. </div>
  201. );
  202. }
  203. }
  204. WBCollectionContent.defaultProps = {
  205. 'collectionPath': '',
  206. 'page': 0,
  207. 'itemsPerPage': 20
  208. };
  209. export default WBCollectionContent;