diff --git a/.env b/.env index 243d5d9137..fa3ff17add 100644 --- a/.env +++ b/.env @@ -64,6 +64,7 @@ # - "deleteRecord" # - "editRecord" # - "exportMail" +# - "downloadAsync" # - "exposeUpload" # - "exportFtp" # - "mainQueue" @@ -105,7 +106,7 @@ # Example with all profiles: # - COMPOSE_FILE=docker-compose.yml:docker-compose.datastores.yml:docker-compose.tools.yml # - COMPOSE_PROFILES=app,setup,gateway-classic,db,elasticsearch,redis,redis-session,rabbitmq,pma,mailhog,assetsInjest,createRecord,deleteRecord,editRecord, -# exportMail,exposeUpload,exportFtp,mainQueue,populateIndex,pullAssets,recordsActions,subdefCreation, +# exportMail,downloadAsync,exposeUpload,exportFtp,mainQueue,populateIndex,pullAssets,recordsActions,subdefCreation, # validationReminder,webhook,writeMetadatas,shareBasket,scheduler,elk,db-backup,phraseanet-saml-sp # @@ -197,7 +198,10 @@ GATEWAY_FASTCGI_HTTPS=off # Content Security Policy (CSP) # security that helps to detect and mitigate certain types of attacks, including Cross-Site Scripting ## @run -GATEWAY_CSP="default-src 'self' 127.0.0.1 https://apiws.carrick-skills.com:8443 https://apiws.carrick-flow.com:8443 https://fonts.gstatic.com *.tiles.mapbox.com https://api.mapbox.com https://events.mapbox.com *.axept.io *.matomo.cloud *.newrelic.com *.nr-data.net https://www.googletagmanager.com *.google-analytics.com *.phrasea.io https://apiws.carrick-flow.com:8443 https://apiws.carrick-skills.com:8443 https://maxcdn.bootstrapcdn.com data: ; script-src 'unsafe-inline' 'unsafe-eval' 'self' https://www.gstatic.com *.alchemyasp.com *.axept.io *.matomo.cloud *.newrelic.com https://www.googletagmanager.com https://apiws.carrick-flow.com:8443 https://apiws.carrick-skills.com:8443 https://maxcdn.bootstrapcdn.com data: blob: ; style-src 'self' 'unsafe-inline' https://fonts.gstatic.com https://fonts.googleapis.com https://www.google.com https://www.gstatic.com https://apiws.carrick-flow.com:8443 https://apiws.carrick-skills.com:8443 https://maxcdn.bootstrapcdn.com ; img-src 'self' data: blob: *.tiles.mapbox.com https://axeptio.imgix.net *.cloudfront.net *.phrasea.io *.amazonaws.com https://apiws.carrick-flow.com:8443 https://apiws.carrick-skills.com:8443 https://maxcdn.bootstrapcdn.com https://www.gnu.org/graphics/ ; object-src 'self'; frame-ancestors 'self'" +## GATEWAY_CSP="default-src 'self' 127.0.0.1 https://apiws.carrick-skills.com:8443 https://apiws.carrick-flow.com:8443 https://fonts.gstatic.com *.tiles.mapbox.com https://api.mapbox.com https://events.mapbox.com *.axept.io *.matomo.cloud *.newrelic.com *.nr-data.net https://www.googletagmanager.com *.google-analytics.com *.phrasea.io https://apiws.carrick-flow.com:8443 https://apiws.carrick-skills.com:8443 https://maxcdn.bootstrapcdn.com data: ; script-src 'unsafe-inline' 'unsafe-eval' 'self' https://www.gstatic.com *.alchemyasp.com *.axept.io *.matomo.cloud *.newrelic.com https://www.googletagmanager.com https://apiws.carrick-flow.com:8443 https://apiws.carrick-skills.com:8443 https://maxcdn.bootstrapcdn.com data: blob: ; style-src 'self' 'unsafe-inline' https://fonts.gstatic.com https://fonts.googleapis.com https://www.google.com https://www.gstatic.com https://apiws.carrick-flow.com:8443 https://apiws.carrick-skills.com:8443 https://maxcdn.bootstrapcdn.com ; img-src 'self' data: blob: *.tiles.mapbox.com https://axeptio.imgix.net *.cloudfront.net *.phrasea.io *.amazonaws.com https://apiws.carrick-flow.com:8443 https://apiws.carrick-skills.com:8443 https://maxcdn.bootstrapcdn.com https://www.gnu.org/graphics/ https://sockjs-eu.pusher.com:443 wss://ws-eu.pusher.com ; object-src 'self'; frame-ancestors 'self'" + + +GATEWAY_CSP="default-src 'self' 127.0.0.1 https://sockjs-eu.pusher.com:443 wss://ws-eu.pusher.com https://apiws.carrick-skills.com:8443 https://apiws.carrick-flow.com:8443 https://fonts.gstatic.com *.tiles.mapbox.com https://api.mapbox.com https://events.mapbox.com *.axept.io *.matomo.cloud *.newrelic.com *.nr-data.net https://www.googletagmanager.com *.google-analytics.com *.phrasea.io https://apiws.carrick-flow.com:8443 https://apiws.carrick-skills.com:8443 data: ;script-src 'unsafe-inline' 'unsafe-eval' 'self' https://www.gstatic.com *.alchemyasp.com *.axept.io *.matomo.cloud *.newrelic.com https://www.googletagmanager.com https://apiws.carrick-flow.com:8443 https://apiws.carrick-skills.com:8443 ;style-src 'self' 'unsafe-inline' https://fonts.gstatic.com https://fonts.googleapis.com https://www.google.com https://www.gstatic.com https://apiws.carrick-flow.com:8443 https://apiws.carrick-skills.com:8443;img-src 'self' data: blob: *.tiles.mapbox.com https://axeptio.imgix.net *.cloudfront.net *.phrasea.io *.amazonaws.com https://apiws.carrick-flow.com:8443 https://apiws.carrick-skills.com:8443 ; object-src 'self';frame-ancestors 'self'" # --- RabbitMQ settings ------------------------------------------------------------------------------------------------ @@ -686,6 +690,9 @@ PHRASEANET_WORKER_editRecord=2 # @run PHRASEANET_WORKER_exportMail=2 +# @run +PHRASEANET_WORKER_downloadAsync=2 + # @run PHRASEANET_WORKER_exposeUpload=2 diff --git a/Phraseanet-production-client/dist/production.js b/Phraseanet-production-client/dist/production.js index 4f2e56c610..005aa6c562 100644 --- a/Phraseanet-production-client/dist/production.js +++ b/Phraseanet-production-client/dist/production.js @@ -7441,6 +7441,7 @@ var exportRecord = function exportRecord(services) { var url = configService.get('baseUrl'); var $container = null; var initialize = function initialize() { + $container = (0, _jquery2.default)('body'); $container.on('click', '.record-export-action', function (event) { event.preventDefault(); @@ -7579,6 +7580,86 @@ var exportRecord = function exportRecord(services) { $dialog.close(); }); + // $('#download_async .download_button_async').bind('click', function () { + // if (!check_subdefs($('#download_async'), dataConfig)) { + // return false; + // } + // + // if (!check_TOU($('#download_async'), dataConfig)) { + // return false; + // } + // + // var total = 0; + // var count = 0; + // + // $('input[name="obj[]"]', $('#download_async')).each(function () { + // var total_el = $( + // '#download_async input[name=download_' + $(this).val() + ']' + // ); + // var count_el = $( + // '#download_async input[name=count_' + $(this).val() + ']' + // ); + // if ($(this).prop('checked')) { + // total += parseInt($(total_el).val(), 10); + // count += parseInt($(count_el).val(), 10); + // } + // }); + // + // if (count > 1 && total / 1024 / 1024 > dataConfig.maxDownload) { + // if ( + // confirm( + // `${dataConfig.msg.fileTooLarge} \n ${dataConfig.msg + // .fileTooLargeAlt}` + // ) + // ) { + // $( + // 'input[name="obj[]"]:checked', + // $('#download_async') + // ).each(function (i, n) { + // $( + // 'input[name="obj[]"][value="' + $(n).val() + '"]', + // $('#sendmail') + // ).prop('checked', true); + // }); + // + // $(document).find('input[name="taglistdestmail"]').tagsinput('add', dataConfig.user.email); + // + // var tabs = $('.tabs', $dialog.getDomElement()); + // tabs.tabs('option', 'active', 1); + // } + // + // return false; + // } + // $('#download_async form').submit(); + + + // // Enable pusher logging - don't include this in production + // Pusher.logToConsole = true; + // const pusher = new Pusher('07b97d8d50b1f2b3d515', { + // cluster: 'eu' + // }); + // const channel = pusher.subscribe("my-channel"); + // pusher.connection.bind("state_change", function (states) { + // // states = {previous: 'oldState', current: 'newState'} + // console.log("========== connection changed : ========== ", states); + // }); + // channel.bind("my-event", (data) => { + // // Method to be dispatched on trigger. + // console.log("========== received from pusher : ========== ", data); + // channel.unbind("my-event"); + // console.log("========== channel unbinded ========== "); + // channel.disconnect(); + // console.log("========== channel disconnected ========== "); + // pusher.unsubscribe("my-channel"); + // console.log("========== pusher unsubscribed ========== "); + // pusher.disconnect() + // console.log("========== pusher disconnected ========== "); + // }); + + + // $dialog.close(); + // }); + (0, _jquery2.default)('#order .order_button').bind('click', function () { var title = ''; if (!check_TOU((0, _jquery2.default)('#order'), dataConfig)) { @@ -7705,31 +7786,7 @@ var exportRecord = function exportRecord(services) { (0, _jquery2.default)('#sendmail form').submit(); humane.infoLarge((0, _jquery2.default)('#export-send-mail-notif').val()); - // $dialog.close(); - - - // Enable pusher logging - don't include this in production - _pusherJs2.default.logToConsole = true; - var pusher = new _pusherJs2.default('07b97d8d50b1f2b3d515', { - cluster: 'eu' - }); - var channel = pusher.subscribe("my-channel"); - pusher.connection.bind("state_change", function (states) { - // states = {previous: 'oldState', current: 'newState'} - console.log("========== connection changed : ========== ", states); - }); - channel.bind("my-event", function (data) { - // Method to be dispatched on trigger. - console.log("========== received from pusher : ========== ", data); - channel.unbind("my-event"); - console.log("========== channel unbinded ========== "); - channel.disconnect(); - console.log("========== channel disconnected ========== "); - pusher.unsubscribe("my-channel"); - console.log("========== pusher unsubscribed ========== "); - pusher.disconnect(); - console.log("========== pusher disconnected ========== "); - }); + $dialog.close(); }); (0, _jquery2.default)('.datepicker', $dialog.getDomElement()).datepicker({ diff --git a/Phraseanet-production-client/dist/production.min.js b/Phraseanet-production-client/dist/production.min.js index 4f2e56c610..005aa6c562 100644 --- a/Phraseanet-production-client/dist/production.min.js +++ b/Phraseanet-production-client/dist/production.min.js @@ -7441,6 +7441,7 @@ var exportRecord = function exportRecord(services) { var url = configService.get('baseUrl'); var $container = null; var initialize = function initialize() { + $container = (0, _jquery2.default)('body'); $container.on('click', '.record-export-action', function (event) { event.preventDefault(); @@ -7579,6 +7580,86 @@ var exportRecord = function exportRecord(services) { $dialog.close(); }); + // $('#download_async .download_button_async').bind('click', function () { + // if (!check_subdefs($('#download_async'), dataConfig)) { + // return false; + // } + // + // if (!check_TOU($('#download_async'), dataConfig)) { + // return false; + // } + // + // var total = 0; + // var count = 0; + // + // $('input[name="obj[]"]', $('#download_async')).each(function () { + // var total_el = $( + // '#download_async input[name=download_' + $(this).val() + ']' + // ); + // var count_el = $( + // '#download_async input[name=count_' + $(this).val() + ']' + // ); + // if ($(this).prop('checked')) { + // total += parseInt($(total_el).val(), 10); + // count += parseInt($(count_el).val(), 10); + // } + // }); + // + // if (count > 1 && total / 1024 / 1024 > dataConfig.maxDownload) { + // if ( + // confirm( + // `${dataConfig.msg.fileTooLarge} \n ${dataConfig.msg + // .fileTooLargeAlt}` + // ) + // ) { + // $( + // 'input[name="obj[]"]:checked', + // $('#download_async') + // ).each(function (i, n) { + // $( + // 'input[name="obj[]"][value="' + $(n).val() + '"]', + // $('#sendmail') + // ).prop('checked', true); + // }); + // + // $(document).find('input[name="taglistdestmail"]').tagsinput('add', dataConfig.user.email); + // + // var tabs = $('.tabs', $dialog.getDomElement()); + // tabs.tabs('option', 'active', 1); + // } + // + // return false; + // } + // $('#download_async form').submit(); + + + // // Enable pusher logging - don't include this in production + // Pusher.logToConsole = true; + // const pusher = new Pusher('07b97d8d50b1f2b3d515', { + // cluster: 'eu' + // }); + // const channel = pusher.subscribe("my-channel"); + // pusher.connection.bind("state_change", function (states) { + // // states = {previous: 'oldState', current: 'newState'} + // console.log("========== connection changed : ========== ", states); + // }); + // channel.bind("my-event", (data) => { + // // Method to be dispatched on trigger. + // console.log("========== received from pusher : ========== ", data); + // channel.unbind("my-event"); + // console.log("========== channel unbinded ========== "); + // channel.disconnect(); + // console.log("========== channel disconnected ========== "); + // pusher.unsubscribe("my-channel"); + // console.log("========== pusher unsubscribed ========== "); + // pusher.disconnect() + // console.log("========== pusher disconnected ========== "); + // }); + + + // $dialog.close(); + // }); + (0, _jquery2.default)('#order .order_button').bind('click', function () { var title = ''; if (!check_TOU((0, _jquery2.default)('#order'), dataConfig)) { @@ -7705,31 +7786,7 @@ var exportRecord = function exportRecord(services) { (0, _jquery2.default)('#sendmail form').submit(); humane.infoLarge((0, _jquery2.default)('#export-send-mail-notif').val()); - // $dialog.close(); - - - // Enable pusher logging - don't include this in production - _pusherJs2.default.logToConsole = true; - var pusher = new _pusherJs2.default('07b97d8d50b1f2b3d515', { - cluster: 'eu' - }); - var channel = pusher.subscribe("my-channel"); - pusher.connection.bind("state_change", function (states) { - // states = {previous: 'oldState', current: 'newState'} - console.log("========== connection changed : ========== ", states); - }); - channel.bind("my-event", function (data) { - // Method to be dispatched on trigger. - console.log("========== received from pusher : ========== ", data); - channel.unbind("my-event"); - console.log("========== channel unbinded ========== "); - channel.disconnect(); - console.log("========== channel disconnected ========== "); - pusher.unsubscribe("my-channel"); - console.log("========== pusher unsubscribed ========== "); - pusher.disconnect(); - console.log("========== pusher disconnected ========== "); - }); + $dialog.close(); }); (0, _jquery2.default)('.datepicker', $dialog.getDomElement()).datepicker({ diff --git a/Phraseanet-production-client/src/components/record/export.js b/Phraseanet-production-client/src/components/record/export.js index bc2feb0226..ed415d89a0 100644 --- a/Phraseanet-production-client/src/components/record/export.js +++ b/Phraseanet-production-client/src/components/record/export.js @@ -162,6 +162,28 @@ const exportRecord = services => { return false; } + // // Enable pusher logging - don't include this in production + // Pusher.logToConsole = true; + // const pusher = new Pusher('07b97d8d50b1f2b3d515', { + // cluster: 'eu' + // }); + // const channel = pusher.subscribe("my-channel"); + // pusher.connection.bind("state_change", function (states) { + // // states = {previous: 'oldState', current: 'newState'} + // console.log("========== connection changed : ========== ", states); + // }); + // channel.bind("my-event", (data) => { + // // Method to be dispatched on trigger. + // console.log("========== received from pusher : ========== ", data); + // channel.unbind("my-event"); + // console.log("========== channel unbinded ========== "); + // channel.disconnect(); + // console.log("========== channel disconnected ========== "); + // pusher.unsubscribe("my-channel"); + // console.log("========== pusher unsubscribed ========== "); + // pusher.disconnect() + // console.log("========== pusher disconnected ========== "); + // }); $('#download form').submit(); $dialog.close(); }); @@ -318,34 +340,7 @@ const exportRecord = services => { $('#sendmail form').submit(); humane.infoLarge($('#export-send-mail-notif').val()); - // $dialog.close(); - - - - // Enable pusher logging - don't include this in production - Pusher.logToConsole = true; - const pusher = new Pusher('07b97d8d50b1f2b3d515', { - cluster: 'eu' - }); - const channel = pusher.subscribe("my-channel"); - pusher.connection.bind("state_change", function (states) { - // states = {previous: 'oldState', current: 'newState'} - console.log("========== connection changed : ========== ", states); - }); - channel.bind("my-event", (data) => { - // Method to be dispatched on trigger. - console.log("========== received from pusher : ========== ", data); - channel.unbind("my-event"); - console.log("========== channel unbinded ========== "); - channel.disconnect(); - console.log("========== channel disconnected ========== "); - pusher.unsubscribe("my-channel"); - console.log("========== pusher unsubscribed ========== "); - pusher.disconnect() - console.log("========== pusher disconnected ========== "); - }); - - + $dialog.close(); }); $('.datepicker', $dialog.getDomElement()).datepicker({ diff --git a/config/configuration.sample.yml b/config/configuration.sample.yml index afb0abf8f1..d68fb0eab7 100644 --- a/config/configuration.sample.yml +++ b/config/configuration.sample.yml @@ -244,6 +244,11 @@ registration-fields: - name: geonameid required: true +pusher: + enabled: false + auth_key: 'pusher-auth_key' + secret: 'pusher-secret' + app_id: 'pusher-app_id' xsendfile: enabled: false type: nginx diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 629c4cb8f7..3c7cbe351a 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -99,8 +99,15 @@ services: w-exportMail: volumes: - - ../:/var/alchemy - - .:/var/alchemy/Phraseanet + - ../:/var/alchemy + - .:/var/alchemy/Phraseanet + networks: + - internal + + w-downloadAsync: + volumes: + - ../:/var/alchemy + - .:/var/alchemy/Phraseanet networks: - internal diff --git a/docker-compose.yml b/docker-compose.yml index 991fb0c4bb..eafb0a9309 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -309,6 +309,7 @@ services: - PHRASEANET_WORKER_deleteRecord - PHRASEANET_WORKER_editRecord - PHRASEANET_WORKER_exportMail + - PHRASEANET_WORKER_downloadAsync - PHRASEANET_WORKER_exposeUpload - PHRASEANET_WORKER_ftp - PHRASEANET_WORKER_mainQueue @@ -629,46 +630,100 @@ services: profiles: ["workers", "exportMail"] restart: on-failure depends_on: - - phraseanet + - phraseanet environment: - - STACK_NAME - - OPCACHE_ENABLED - - SESSION_CACHE_LIMITER - - PHP_LOG_LEVEL - - PHP_CLI_MEMORY_LIMIT - - LC_MESSAGES=C.UTF-8 - - LC_COLLATE=C.UTF-8 - - LC_IDENTIFICATION=C.UTF-8 - - LANG=C.UTF-8 - - LC_MEASUREMENT=C.UTF-8 - - LC_CTYPE=C.UTF-8 - - LC_TIME=C.UTF-8 - - LC_NAME=C.UTF-8 - - PHRASEANET_EXPLODE_WORKER - - PHRASEANET_WORKERS_LAUNCH_METHOD - - PHRASEANET_WORKER_exportMail - - IMAGEMAGICK_POLICY_VERSION - - IMAGEMAGICK_POLICY_WIDTH - - IMAGEMAGICK_POLICY_HEIGHT - - IMAGEMAGICK_POLICY_MAP - - IMAGEMAGICK_POLICY_MEMORY - - IMAGEMAGICK_POLICY_AREA - - IMAGEMAGICK_POLICY_DISK - - IMAGEMAGICK_POLICY_TEMPORARY_PATH - - NEWRELIC_ENABLED - - NEWRELIC_LICENSE_KEY - - NEWRELIC_APP_NAME - - BLACKFIRE_ENABLED - - BLACKFIRE_SERVER_ID - - BLACKFIRE_SERVER_TOKEN + - STACK_NAME + - OPCACHE_ENABLED + - SESSION_CACHE_LIMITER + - PHP_LOG_LEVEL + - PHP_CLI_MEMORY_LIMIT + - LC_MESSAGES=C.UTF-8 + - LC_COLLATE=C.UTF-8 + - LC_IDENTIFICATION=C.UTF-8 + - LANG=C.UTF-8 + - LC_MEASUREMENT=C.UTF-8 + - LC_CTYPE=C.UTF-8 + - LC_TIME=C.UTF-8 + - LC_NAME=C.UTF-8 + - PHRASEANET_EXPLODE_WORKER + - PHRASEANET_WORKERS_LAUNCH_METHOD + - PHRASEANET_WORKER_exportMail + - IMAGEMAGICK_POLICY_VERSION + - IMAGEMAGICK_POLICY_WIDTH + - IMAGEMAGICK_POLICY_HEIGHT + - IMAGEMAGICK_POLICY_MAP + - IMAGEMAGICK_POLICY_MEMORY + - IMAGEMAGICK_POLICY_AREA + - IMAGEMAGICK_POLICY_DISK + - IMAGEMAGICK_POLICY_TEMPORARY_PATH + - NEWRELIC_ENABLED + - NEWRELIC_LICENSE_KEY + - NEWRELIC_APP_NAME + - BLACKFIRE_ENABLED + - BLACKFIRE_SERVER_ID + - BLACKFIRE_SERVER_TOKEN volumes: - - ${PHRASEANET_CONFIG_DIR}:/var/alchemy/Phraseanet/config:rw - - ${PHRASEANET_LOGS_DIR}:/var/alchemy/Phraseanet/logs:rw - - ${PHRASEANET_DATA_DIR}:/var/alchemy/Phraseanet/datas:rw - - ${PHRASEANET_THUMBNAILS_DIR}:/var/alchemy/Phraseanet/www/thumbnails:rw - - ${PHRASEANET_CUSTOM_DIR}:/var/alchemy/Phraseanet/www/custom:rw - - ${PHRASEANET_CACHE_DIR}:/var/alchemy/Phraseanet/cache:rw - - ${PHRASEANET_TMP_DIR}:/var/alchemy/Phraseanet/tmp:rw + - ${PHRASEANET_CONFIG_DIR}:/var/alchemy/Phraseanet/config:rw + - ${PHRASEANET_LOGS_DIR}:/var/alchemy/Phraseanet/logs:rw + - ${PHRASEANET_DATA_DIR}:/var/alchemy/Phraseanet/datas:rw + - ${PHRASEANET_THUMBNAILS_DIR}:/var/alchemy/Phraseanet/www/thumbnails:rw + - ${PHRASEANET_CUSTOM_DIR}:/var/alchemy/Phraseanet/www/custom:rw + - ${PHRASEANET_CACHE_DIR}:/var/alchemy/Phraseanet/cache:rw + - ${PHRASEANET_TMP_DIR}:/var/alchemy/Phraseanet/tmp:rw + networks: + - internal + + w-downloadAsync: + build: + context: . + target: phraseanet-worker + args: + - SSH_PRIVATE_KEY=${PHRASEANET_SSH_PRIVATE_KEY} + - PHRASEANET_PLUGINS=${PHRASEANET_PLUGINS} + image: $PHRASEANET_DOCKER_REGISTRY/phraseanet-worker:$PHRASEANET_DOCKER_TAG + profiles: ["workers", "exportMail"] + restart: on-failure + depends_on: + - phraseanet + environment: + - STACK_NAME + - OPCACHE_ENABLED + - SESSION_CACHE_LIMITER + - PHP_LOG_LEVEL + - PHP_CLI_MEMORY_LIMIT + - LC_MESSAGES=C.UTF-8 + - LC_COLLATE=C.UTF-8 + - LC_IDENTIFICATION=C.UTF-8 + - LANG=C.UTF-8 + - LC_MEASUREMENT=C.UTF-8 + - LC_CTYPE=C.UTF-8 + - LC_TIME=C.UTF-8 + - LC_NAME=C.UTF-8 + - PHRASEANET_EXPLODE_WORKER + - PHRASEANET_WORKERS_LAUNCH_METHOD + - PHRASEANET_WORKER_exportMail + - IMAGEMAGICK_POLICY_VERSION + - IMAGEMAGICK_POLICY_WIDTH + - IMAGEMAGICK_POLICY_HEIGHT + - IMAGEMAGICK_POLICY_MAP + - IMAGEMAGICK_POLICY_MEMORY + - IMAGEMAGICK_POLICY_AREA + - IMAGEMAGICK_POLICY_DISK + - IMAGEMAGICK_POLICY_TEMPORARY_PATH + - NEWRELIC_ENABLED + - NEWRELIC_LICENSE_KEY + - NEWRELIC_APP_NAME + - BLACKFIRE_ENABLED + - BLACKFIRE_SERVER_ID + - BLACKFIRE_SERVER_TOKEN + volumes: + - ${PHRASEANET_CONFIG_DIR}:/var/alchemy/Phraseanet/config:rw + - ${PHRASEANET_LOGS_DIR}:/var/alchemy/Phraseanet/logs:rw + - ${PHRASEANET_DATA_DIR}:/var/alchemy/Phraseanet/datas:rw + - ${PHRASEANET_THUMBNAILS_DIR}:/var/alchemy/Phraseanet/www/thumbnails:rw + - ${PHRASEANET_CUSTOM_DIR}:/var/alchemy/Phraseanet/www/custom:rw + - ${PHRASEANET_CACHE_DIR}:/var/alchemy/Phraseanet/cache:rw + - ${PHRASEANET_TMP_DIR}:/var/alchemy/Phraseanet/tmp:rw networks: - internal diff --git a/docker/phraseanet/worker/supervisor_conf.d/downloadAsync.conf b/docker/phraseanet/worker/supervisor_conf.d/downloadAsync.conf new file mode 100644 index 0000000000..f412ca9493 --- /dev/null +++ b/docker/phraseanet/worker/supervisor_conf.d/downloadAsync.conf @@ -0,0 +1,28 @@ +[program:w-downloadAsync] +command=nice -n 15 /usr/local/bin/php /var/alchemy/Phraseanet/bin/console worker:execute --queue-name=downloadAsync -m $PHRASEANET_WORKER_downloadAsync ; the program (relative uses PATH, can take args) +stdout_logfile=AUTO ; stdout log path, NONE for none; default AUTO +stderr_logfile=AUTO ; stderr log path, NONE for none; default AUTO +process_name=%(program_name)s ; process_name expr (default %(program_name)s) +numprocs=1 ; number of processes copies to start (def 1) +directory=/tmp ; directory to cwd to before exec (def no cwd) +priority=999 ; the relative start priority (default 999) +autostart=true ; start at supervisord start (default: true) +autorestart=true ; whether/when to restart (default: unexpected) +startsecs=0 ; number of secs prog must stay running (def. 1) +startretries=3 ; max # of serial start failures (default 3) +exitcodes=0,2 ; 'expected' exit codes for process (default 0,2) +stopsignal=INT ; signal used to kill process (default TERM) +stopwaitsecs=20 ; max num secs to wait b4 SIGKILL (default 10) +stopasgroup=true ; send stop signal to the UNIX process group (default false) +killasgroup=true ; SIGKILL the UNIX process group (def false) +redirect_stderr=true ; redirect proc stderr to stdout (default false) +user=1000 ; setuid to this UNIX account to run the program +stdout_logfile_maxbytes=50MB ; max # logfile bytes b4 rotation (default 50MB) +stdout_logfile_backups=10 ; # of stdout logfile backups (default 10) +stdout_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0) +stdout_events_enabled=false ; emit events on stdout writes (default false) +stderr_logfile_maxbytes=10MB ; max # logfile bytes b4 rotation (default 50MB) +stderr_logfile_backups=10 ; # of stderr logfile backups (default 10) +stderr_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0) +stderr_events_enabled=false ; emit events on stderr writes (default false) +environment=HOME=/home/app,USER=app ; process environment additions (def no adds) diff --git a/lib/Alchemy/Phrasea/Controller/Prod/DownloadController.php b/lib/Alchemy/Phrasea/Controller/Prod/DownloadController.php index d5105adfc1..d4ddfa74d0 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/DownloadController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/DownloadController.php @@ -11,11 +11,14 @@ use Alchemy\Phrasea\Application\Helper\DispatcherAware; use Alchemy\Phrasea\Controller\Controller; +use Alchemy\Phrasea\Core\Configuration\PropertyAccess; +use Alchemy\Phrasea\Core\Event\DownloadAsyncEvent; use Alchemy\Phrasea\Core\Event\ExportEvent; use Alchemy\Phrasea\Core\PhraseaEvents; use Alchemy\Phrasea\Model\Manipulator\TokenManipulator; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; class DownloadController extends Controller { @@ -54,14 +57,102 @@ public function checkDownload(Request $request) $list['export_name'] = sprintf('%s.zip', $download->getExportName()); $token = $this->getTokenManipulator()->createDownloadToken($this->getAuthenticatedUser(), serialize($list)); + $this->getDispatcher()->dispatch(PhraseaEvents::EXPORT_CREATE, new ExportEvent( - $this->getAuthenticatedUser(), $ssttid, $lst, $subdefs, $download->getExportName()) + $this->getAuthenticatedUser(), + $ssttid, + $lst, + $subdefs, + $download->getExportName() + ) ); /** @see DoDownloadController::prepareDownload */ return $this->app->redirectPath('prepare_download', ['token' => $token->getValue()]); } + /** + * Download a set of documents + * + * @param Request $request + * @return Response + */ + public function checkDownloadAsync(Request $request) + { + if (!$this->isCrsfValid($request, 'prodExportDownload')) { + $this->app->abort(403); + } + + $lst = $request->request->get('lst'); + $ssttid = $request->request->get('ssttid', ''); + $subdefs = $request->request->get('obj', []); + + $download = new \set_export($this->app, $lst, $ssttid); + + if (0 === $download->get_total_download()) { + $this->app->abort(403); + } + + /** @see \set_export::prepare_export */ + $list = $download->prepare_export( + $this->getAuthenticatedUser(), + $this->app['filesystem'], + $subdefs, + $request->request->get('type') === 'title' ? true : false, + $request->request->get('businessfields'), + \set_export::STAMP_ASYNC, + true + ); + $list['export_name'] = sprintf('%s.zip', $download->getExportName()); + + $records = []; + + foreach ($list['files'] as $file) { + if (!is_array($file) || !isset($file['base_id']) || !isset($file['record_id'])) { + continue; + } + $sbasId = \phrasea::sbasFromBas($this->app, $file['base_id']); + + try { + $record = new \record_adapter($this->app, $sbasId, $file['record_id']); + } catch (\Exception $e) { + continue; + } + + $records[sprintf('%s_%s', $sbasId, $file['record_id'])] = $record; + } + + $token = $this->getTokenManipulator()->createDownloadToken($this->getAuthenticatedUser(), serialize($list)); + + $url = $this->app->url('prepare_download', ['token' => $token->getValue(), 'anonymous' => false, 'type' => \Session_Logger::EVENT_EXPORTMAIL]); + + // ask the worker to build the zip + $this->dispatch(PhraseaEvents::DOWNLOAD_ASYNC_CREATE, new DownloadAsyncEvent( + $this->getAuthenticatedUser()->getId(), + $token->getValue(), + [ + 'url' => $url, + 'ssttid' => $ssttid, + 'lst' => $lst, + ] + )); + + $pusher_auth_key =$this->getConf()->get(['pusher', 'enabled'], false) ? $this->getConf()->get(['pusher', 'auth_key'], '') : null; + return new Response($this->render( + /** @uses templates/web/prod/actions/Download/prepare_async.html.twig */ + '/prod/actions/Download/prepare_async.html.twig', [ + 'module_name' => $this->app->trans('Export'), + 'module' => $this->app->trans('Export'), + 'list' => $list, + 'records' => $records, + 'token' => $token, + 'anonymous' => $request->query->get('anonymous', false), + 'type' => $request->query->get('type', \Session_Logger::EVENT_EXPORTDOWNLOAD), + 'pusher_auth_key' => $pusher_auth_key + ])); + + } + /** * @return TokenManipulator */ @@ -69,4 +160,13 @@ private function getTokenManipulator() { return $this->app['manipulator.token']; } + + /** + * @return PropertyAccess + */ + protected function getConf() + { + return $this->app['conf']; + } + } diff --git a/lib/Alchemy/Phrasea/ControllerProvider/Prod/Download.php b/lib/Alchemy/Phrasea/ControllerProvider/Prod/Download.php index dca334df5b..147b9800eb 100644 --- a/lib/Alchemy/Phrasea/ControllerProvider/Prod/Download.php +++ b/lib/Alchemy/Phrasea/ControllerProvider/Prod/Download.php @@ -45,6 +45,10 @@ public function connect(Application $app) $controllers->before(new OAuthListener(['exit_not_present' => false])); $this->getFirewall($app)->addMandatoryAuthentication($controllers); + /** @uses DownloadController::checkDownloadAsync */ + $controllers->post('/async/', 'controller.prod.download:checkDownloadAsync') + ->bind('check_download_async'); + /** @uses DownloadController::checkDownload */ $controllers->post('/', 'controller.prod.download:checkDownload') ->bind('check_download'); diff --git a/lib/Alchemy/Phrasea/Core/Event/DownloadAsyncEvent.php b/lib/Alchemy/Phrasea/Core/Event/DownloadAsyncEvent.php new file mode 100644 index 0000000000..55bce794dc --- /dev/null +++ b/lib/Alchemy/Phrasea/Core/Event/DownloadAsyncEvent.php @@ -0,0 +1,48 @@ +userId = $userId; + $this->tokenValue = $tokenValue; + $this->params = $params; + } + + public function getTokenValue() + { + return $this->tokenValue; + } + + public function getParams() + { + return $this->params; + } + + /** + * @return mixed + */ + public function getUserId() + { + return $this->userId; + } +} diff --git a/lib/Alchemy/Phrasea/Core/PhraseaEvents.php b/lib/Alchemy/Phrasea/Core/PhraseaEvents.php index 7f231d3802..8f9d31d833 100644 --- a/lib/Alchemy/Phrasea/Core/PhraseaEvents.php +++ b/lib/Alchemy/Phrasea/Core/PhraseaEvents.php @@ -52,6 +52,7 @@ final class PhraseaEvents const EXPORT_MAIL_FAILURE = 'export.mail-failure'; const EXPORT_CREATE = 'export.create'; const EXPORT_MAIL_CREATE = 'export.mail-create'; + const DOWNLOAD_ASYNC_CREATE = 'download.async-create'; const RECORD_EDIT = 'record.edit'; const RECORD_UPLOAD = 'record.upload'; diff --git a/lib/Alchemy/Phrasea/WorkerManager/Provider/AlchemyWorkerServiceProvider.php b/lib/Alchemy/Phrasea/WorkerManager/Provider/AlchemyWorkerServiceProvider.php index 76011a76a3..b5c8df60d6 100644 --- a/lib/Alchemy/Phrasea/WorkerManager/Provider/AlchemyWorkerServiceProvider.php +++ b/lib/Alchemy/Phrasea/WorkerManager/Provider/AlchemyWorkerServiceProvider.php @@ -9,6 +9,7 @@ use Alchemy\Phrasea\WorkerManager\Worker\AssetsIngestWorker; use Alchemy\Phrasea\WorkerManager\Worker\CreateRecordWorker; use Alchemy\Phrasea\WorkerManager\Worker\DeleteRecordWorker; +use Alchemy\Phrasea\WorkerManager\Worker\DownloadAsyncWorker; use Alchemy\Phrasea\WorkerManager\Worker\EditRecordWorker; use Alchemy\Phrasea\WorkerManager\Worker\ExportMailWorker; use Alchemy\Phrasea\WorkerManager\Worker\ExposeUploadWorker; @@ -108,6 +109,12 @@ public function register(Application $app) ->setDelivererLocator(new LazyLocator($app, 'notification.deliverer')); })); + $app['alchemy_worker.type_based_worker_resolver']->addFactory(MessagePublisher::DOWNLOAD_ASYNC_TYPE, new CallableWorkerFactory(function () use ($app) { + return (new DownloadAsyncWorker($app, $app['conf'])) + ->setFileSystemLocator(new LazyLocator($app, 'filesystem')) + ->setDelivererLocator(new LazyLocator($app, 'notification.deliverer')); + })); + $app['alchemy_worker.type_based_worker_resolver']->addFactory(MessagePublisher::ASSETS_INGEST_TYPE, new CallableWorkerFactory(function () use ($app) { return (new AssetsIngestWorker($app)) ->setEntityManagerLocator(new LazyLocator($app, 'orm.em')); diff --git a/lib/Alchemy/Phrasea/WorkerManager/Queue/AMQPConnection.php b/lib/Alchemy/Phrasea/WorkerManager/Queue/AMQPConnection.php index cfad841912..43d6ff48a0 100644 --- a/lib/Alchemy/Phrasea/WorkerManager/Queue/AMQPConnection.php +++ b/lib/Alchemy/Phrasea/WorkerManager/Queue/AMQPConnection.php @@ -69,6 +69,11 @@ class AMQPConnection self::MAX_RETRY => self::DEFAULT_MAX_RETRY_VALUE, self::TTL_RETRY => self::DEFAULT_RETRY_DELAY_VALUE, ], + MessagePublisher::DOWNLOAD_ASYNC_TYPE => [ + 'with' => self::WITH_RETRY, + self::MAX_RETRY => self::DEFAULT_MAX_RETRY_VALUE, + self::TTL_RETRY => self::DEFAULT_RETRY_DELAY_VALUE, + ], MessagePublisher::EXPOSE_UPLOAD_TYPE => [ 'with' => self::WITH_RETRY, self::MAX_RETRY => self::DEFAULT_MAX_RETRY_VALUE, diff --git a/lib/Alchemy/Phrasea/WorkerManager/Queue/MessagePublisher.php b/lib/Alchemy/Phrasea/WorkerManager/Queue/MessagePublisher.php index 57c5049d2a..476bdadb5f 100644 --- a/lib/Alchemy/Phrasea/WorkerManager/Queue/MessagePublisher.php +++ b/lib/Alchemy/Phrasea/WorkerManager/Queue/MessagePublisher.php @@ -15,6 +15,7 @@ class MessagePublisher const CREATE_RECORD_TYPE = 'createRecord'; const DELETE_RECORD_TYPE = 'deleteRecord'; const EXPORT_MAIL_TYPE = 'exportMail'; + const DOWNLOAD_ASYNC_TYPE = 'downloadAsync'; const EXPOSE_UPLOAD_TYPE = 'exposeUpload'; const FTP_TYPE = 'ftp'; const POPULATE_INDEX_TYPE = 'populateIndex'; diff --git a/lib/Alchemy/Phrasea/WorkerManager/Subscriber/ExportSubscriber.php b/lib/Alchemy/Phrasea/WorkerManager/Subscriber/ExportSubscriber.php index bbfde318bd..25ba2250d6 100644 --- a/lib/Alchemy/Phrasea/WorkerManager/Subscriber/ExportSubscriber.php +++ b/lib/Alchemy/Phrasea/WorkerManager/Subscriber/ExportSubscriber.php @@ -2,6 +2,7 @@ namespace Alchemy\Phrasea\WorkerManager\Subscriber; +use Alchemy\Phrasea\Core\Event\DownloadAsyncEvent; use Alchemy\Phrasea\Core\Event\ExportMailEvent; use Alchemy\Phrasea\Core\PhraseaEvents; use Alchemy\Phrasea\WorkerManager\Event\ExportFtpEvent; @@ -20,6 +21,20 @@ public function __construct(MessagePublisher $messagePublisher) $this->messagePublisher = $messagePublisher; } + public function onDownloadAsyncCreate(DownloadAsyncEvent $event) + { + $payload = [ + 'message_type' => MessagePublisher::DOWNLOAD_ASYNC_TYPE, + 'payload' => [ + 'userId' => $event->getUserId(), + 'tokenValue' => $event->getTokenValue(), + 'params' => serialize($event->getParams()) + ] + ]; + + $this->messagePublisher->publishMessage($payload, MessagePublisher::DOWNLOAD_ASYNC_TYPE); + } + public function onExportMailCreate(ExportMailEvent $event) { $payload = [ @@ -73,6 +88,7 @@ public function onExportFtp(ExportFtpEvent $event) public static function getSubscribedEvents() { return [ + PhraseaEvents::DOWNLOAD_ASYNC_CREATE => 'onDownloadAsyncCreate', PhraseaEvents::EXPORT_MAIL_CREATE => 'onExportMailCreate', WorkerEvents::EXPORT_MAIL_FAILURE => 'onExportMailFailure', WorkerEvents::EXPORT_FTP => 'onExportFtp' diff --git a/lib/Alchemy/Phrasea/WorkerManager/Worker/DownloadAsyncWorker.php b/lib/Alchemy/Phrasea/WorkerManager/Worker/DownloadAsyncWorker.php new file mode 100644 index 0000000000..fc7785bba3 --- /dev/null +++ b/lib/Alchemy/Phrasea/WorkerManager/Worker/DownloadAsyncWorker.php @@ -0,0 +1,251 @@ +app = $app; + $this->conf = $conf; + } + + public function process(array $payload) + { + $this->repoWorkerJob = $this->getWorkerRunningJobRepository(); + $em = $this->repoWorkerJob->getEntityManager(); + $em->beginTransaction(); + $this->repoWorkerJob->reconnect(); + $date = new \DateTime(); + + $message = [ + 'message_type' => MessagePublisher::DOWNLOAD_ASYNC_TYPE, + 'payload' => $payload + ]; + + try { + $workerRunningJob = new WorkerRunningJob(); + $workerRunningJob + ->setWork(MessagePublisher::DOWNLOAD_ASYNC_TYPE) + ->setPayload($message) + ->setPublished($date->setTimestamp($payload['published'])) + ->setStatus(WorkerRunningJob::RUNNING) + ; + + $em->persist($workerRunningJob); + + $em->flush(); + + $em->commit(); + } catch (\Exception $e) { + $em->rollback(); + $workerRunningJob = null; + } + + $filesystem = $this->getFilesystem(); + + $params = unserialize($payload['params']); + + /** @var UserRepository $userRepository */ + $userRepository = $this->app['repo.users']; + + $user = $userRepository->find($payload['userId']); + $localeEmitter = $user->getLocale(); + + /** @var TokenRepository $tokenRepository */ + $tokenRepository = $this->app['repo.tokens']; + + /** @var Token $token */ + $token = $tokenRepository->findValidToken($payload['tokenValue']); + + if($this->conf->get(['pusher', 'enabled'], false)) { + $options = array( + 'cluster' => 'eu', + 'useTLS' => true + ); + try { + $this->pusher = new Pusher( + $this->conf->get(['pusher', 'auth_key'], ''), + $this->conf->get(['pusher', 'secret'], ''), + $this->conf->get(['pusher', 'app_id'], ''), + $options + ); + $this->pusher_channel_name = $token->getValue(); + } + catch (\Exception $e) { + // no-op + } + } + + $list = unserialize($token->getData()); + + foreach($list['files'] as $k_file => $v_file) { + foreach($v_file['subdefs'] as $k_subdef => $v_subdef) { + if($k_subdef === "document" && $v_subdef['to_stamp']) { + // we must stamp this document + try { + $record = $this->app->getApplicationBox()->get_databox($v_file['databox_id'])->get_record($v_file['record_id']); + $sd = $record->get_subdef($k_subdef); + if(!is_null($path = \recordutils_image::stamp($this->app, $sd))) { + // stamped ! + $pi = pathinfo($path); + $list['files'][$k_file]['subdefs'][$k_subdef]['path'] = $pi['dirname']; + $list['files'][$k_file]['subdefs'][$k_subdef]['file'] = $pi['basename']; + $list['files'][$k_file]['subdefs'][$k_subdef]['size'] = filesize($path); + } + } + catch (\Exception $e) { + // failed to stamp ? ignore and send the original file + } + } + if($list['files'][$k_file]['subdefs'][$k_subdef]['size'] > 0) { + $this->push( + 'file_ok', + [ + 'message' => "", + 'databox_id' => $list['files'][$k_file]['databox_id'], + 'record_id' => $list['files'][$k_file]['record_id'], + 'subdef' => $k_subdef, + 'size' => $list['files'][$k_file]['subdefs'][$k_subdef]['size'], + 'human_size' => $this->getHumanSize($list['files'][$k_file]['subdefs'][$k_subdef]['size']), + ] + ); + } + } + } + + $caption_dir = null; + // add the captions files if exist + foreach ($list['captions'] as $v_caption) { + if (!$caption_dir) { + // do this only once + $caption_dir = $this->app['tmp.caption.path'] . '/' . time() . $payload['userId'] . '/'; + $filesystem->mkdir($caption_dir, 0750); + } + + $subdefName = $v_caption['subdefName']; + $kFile = $v_caption['fileId']; + + $download_element = new \record_exportElement( + $this->app, + $list['files'][$kFile]['databox_id'], + $list['files'][$kFile]['record_id'], + $v_caption['elementDirectory'], + $v_caption['remain_hd'], + $user + ); + + $file = $list['files'][$kFile]["export_name"] + . $list['files'][$kFile]["subdefs"][$subdefName]["ajout"] . '.' + . $list['files'][$kFile]["subdefs"][$subdefName]["exportExt"]; + + $desc = $this->app['serializer.caption']->serialize($download_element->get_caption(), $v_caption['serializeMethod'], $v_caption['businessFields']); + file_put_contents($caption_dir . $file, $desc); + + $list['files'][$kFile]["subdefs"][$subdefName]["path"] = $caption_dir; + $list['files'][$kFile]["subdefs"][$subdefName]["file"] = $file; + $list['files'][$kFile]["subdefs"][$subdefName]["size"] = filesize($caption_dir . $file); + $list['files'][$kFile]["subdefs"][$subdefName]['businessfields'] = $v_caption['businessFields']; + + $this->push( + 'file_ok', + [ + 'message' => "", + 'databox_id' => $list['files'][$kFile]['databox_id'], + 'record_id' => $list['files'][$kFile]['record_id'], + 'subdef' => $subdefName, + 'size' => $list['files'][$kFile]["subdefs"][$subdefName]["size"], + 'human_size' => $this->getHumanSize($list['files'][$kFile]["subdefs"][$subdefName]["size"]), + ] + ); + } + + $this->repoWorkerJob->reconnect(); + //zip documents + \set_export::build_zip( + $this->app, + $token, + $list, + $this->app['tmp.download.path'].'/'. $token->getValue() . '.zip' + ); + + if ($workerRunningJob != null) { + $this->repoWorkerJob->reconnect(); + $workerRunningJob + ->setStatus(WorkerRunningJob::FINISHED) + ->setFinished(new \DateTime('now')) + ; + + $em->persist($workerRunningJob); + + $em->flush(); + } + + sleep(1); + + $this->push('zip_ready', ['message' => ""]); + } + + private function push(string $event, $data) + { + if($this->pusher) { + $r = $this->pusher->trigger( + $this->pusher_channel_name, + $event, + $data + ); + } + } + + // todo : this Ko;Mo;Go code already exists in phraseanet (download) + private function getHumanSize(int $size) + { + $unit = 'octets'; + $units = ['Go', 'Mo', 'Ko']; + $format = "%d %s"; + while ($size > 1024 && !empty($units)) { + $unit = array_pop($units); + $size /= 1024.0; + $format = "%.02f %s"; + } + return sprintf($format, $size, $unit); + } + + + /** + * @return WorkerRunningJobRepository + */ + private function getWorkerRunningJobRepository() + { + return $this->app['repo.worker-running-job']; + } +} diff --git a/package.json b/package.json index c9852eb936..ed013eae32 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "normalize-css": "^2.1.0", "npm": "^6.0.0", "npm-modernizr": "^2.8.3", + "pusher-js": "^8.3.0", "requirejs": "^2.3.5", "tinymce": "^4.0.28", "underscore": "^1.8.3", diff --git a/resources/gulp/build.js b/resources/gulp/build.js index 80ef15dcf6..9ac912d5e5 100644 --- a/resources/gulp/build.js +++ b/resources/gulp/build.js @@ -50,6 +50,7 @@ gulp.task('build-vendors', [ 'build-jquery-lazyload', 'build-jquery-test-paths', 'build-simple-colorpicker', - 'build-jquery-datetimepicker' + 'build-jquery-datetimepicker', + 'build-pusher-js' ], function () { }); diff --git a/resources/gulp/components/vendors/pusher-js.js b/resources/gulp/components/vendors/pusher-js.js new file mode 100644 index 0000000000..c22b3dbdba --- /dev/null +++ b/resources/gulp/components/vendors/pusher-js.js @@ -0,0 +1,8 @@ +var gulp = require('gulp'); +var config = require('../../config.js'); +var utils = require('../../utils.js'); + +gulp.task('build-pusher-js', [], function(){ + return gulp.src([config.paths.nodes + 'pusher-js/dist/web/**']) + .pipe(gulp.dest(config.paths.build + 'vendors/pusher-js')); +}); diff --git a/templates/web/common/dialog_export.html.twig b/templates/web/common/dialog_export.html.twig index 466f50db13..36502f10c3 100644 --- a/templates/web/common/dialog_export.html.twig +++ b/templates/web/common/dialog_export.html.twig @@ -95,7 +95,6 @@
{{ "Base" | trans }} | +{{ "Name" | trans }} | +{{ "Sub definition" | trans }} | +{{ "Size" | trans }} | +{{ "Thumbnail" | trans }} | +
---|---|---|---|---|
{{ app|sbas_from_bas(file['base_id'])|sbas_labels(app) }} {{ file['base_id']|bas_labels(app) }} | +{{ file['original_name'] }} | +
+ {% if file['subdefs'] is iterable and file['subdefs']|length > 0 %}
+
|
+ {{ size|formatOctets }} | ++ {% set record_key = app|sbas_from_bas(file['base_id']) ~'_'~ file['record_id']%} + + {% if record_key in records|keys %} + {% set record = attribute(records, record_key) %} + {% set thumbnail = record.get_thumbnail() %} + {% if thumbnail.isLandscape() %} + {% set w = 140 %} + {% if thumbnail.get_height() > 0 and thumbnail.get_width() > 0 %} + {% set h = (w / (thumbnail.get_width() / thumbnail.get_height()))|round %} + {% else %} + {% set h = 140 %} + {% endif %} + {% else %} + {% set h = 105 %} + {% if thumbnail.get_height() > 0 %} + {% set w = (h * (thumbnail.get_width() / thumbnail.get_height()))|round %} + {% else %} + {% set w = 105 %} + {% endif %} + {% endif %} + + + {% endif %} + | +