diff --git a/CrayFits/.env b/CrayFits/.env new file mode 100644 index 00000000..05ea69dd --- /dev/null +++ b/CrayFits/.env @@ -0,0 +1,34 @@ +# In all environments, the following files are loaded if they exist, +# the later taking precedence over the former: +# +# * .env contains default values for the environment variables needed by the app +# * .env.local uncommitted file with local overrides +# * .env.$APP_ENV committed environment-specific defaults +# * .env.$APP_ENV.local uncommitted environment-specific overrides +# +# Real environment variables win over .env files. +# +# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# +# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). +# https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration + +###> symfony/framework-bundle ### +APP_ENV=dev +APP_SECRET=5273c9fd27359cc05a2f653521d8add0 +#TRUSTED_PROXIES=127.0.0.1,127.0.0.2 +#TRUSTED_HOSTS='^localhost|example\.com$' +###< symfony/framework-bundle ### + +###> doctrine/doctrine-bundle ### +# Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db" +# Configure your db driver and server_version in config/packages/doctrine.yaml +DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name +###< doctrine/doctrine-bundle ### +FITS_WEBSERVICE_URI=http://localhost:8080/fits/ + + +###> nelmio/cors-bundle ### +CORS_ALLOW_ORIGIN=^https?://localhost(:[0-9]+)?$ +###< nelmio/cors-bundle ### diff --git a/CrayFits/README.md b/CrayFits/README.md new file mode 100644 index 00000000..437fda41 --- /dev/null +++ b/CrayFits/README.md @@ -0,0 +1,66 @@ +# CrayFits + +## Introduction + +[FITS][1] as a microservice. + +## Requirements + +* Tomcat +* [Composer][2] + +## Installation + +### Install FITS webservice + +* Download the latest `fits.zip` and `fits.war` from +[https://projects.iq.harvard.edu/fits/downloads](https://projects.iq.harvard.edu/fits/downloads). +You may need to install a zip library to unzip the file. +* Copy the `.war` file to your Tomcat webapps directory and test. +* Edit the Tomcat `conf/catalina.properties` file by adding the +following two lines to the bottom of the file: +```properties +fits.home=/\/fits +shared.loader=/\/fits/lib/*.jar +``` +* Restart Tomcat. +* Test the webservice with: +```bash +curl -k -F datafile="@/path/to/myfile.jpg" http://[tomcat_domain]:[tomcat_port]/fits/examine +``` +(note: the ‘@’ is required.) + +### Install CrayFITS microservice + +* Clone this repository somewhere in your web root. +* `$ cd /path/to/CrayFits` and run `$ composer install` +* For production, configure your web server appropriately (e.g. add a VirtualHost for CrayFits in Apache) + +To run the microservice on the Symfony Console, enter: +```bash +php bin/console server:start *:8050 +``` +in the microservice root folder. + +The server is stopped with: +```bash +php bin/console server:stop +``` +On a production machine you'd probably want to configure an additional +port in Apache. + +Note: The location of the FITS webserver is stored in the `.env` file in the +root dir of the Symfony app. This will have to be reconfigured if the FITS +server is anywhere other than `localhost:8080/fits` + + +#### Optional: Configure Alpaca to accept derivative requests from Islandora. + +To use Alpaca as an interface to this microservice, configure +an [`islandora-connector-derivative`](https://github.com/Islandora/Alpaca#islandora-connector-derivative) +appropriate to your installation of CrayFits. + +[1]: https://projects.iq.harvard.edu/fits +[2]: https://getcomposer.org/download/ + + diff --git a/CrayFits/bin/console b/CrayFits/bin/console new file mode 100644 index 00000000..19c2f6c3 --- /dev/null +++ b/CrayFits/bin/console @@ -0,0 +1,42 @@ +#!/usr/bin/env php +getParameterOption(['--env', '-e'], null, true)) { + putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env); +} + +if ($input->hasParameterOption('--no-debug', true)) { + putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); +} + +require dirname(__DIR__).'/config/bootstrap.php'; + +if ($_SERVER['APP_DEBUG']) { + umask(0000); + + if (class_exists(Debug::class)) { + Debug::enable(); + } +} + +$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); +$application = new Application($kernel); +$application->run($input); diff --git a/CrayFits/composer.json b/CrayFits/composer.json new file mode 100644 index 00000000..f1ad856f --- /dev/null +++ b/CrayFits/composer.json @@ -0,0 +1,67 @@ +{ + "type": "project", + "license": "proprietary", + "require": { + "ext-ctype": "*", + "ext-iconv": "*", + "guzzlehttp/guzzle": "^6.3", + "monolog/monolog": "^1.24", + "symfony/console": "^4.4", + "symfony/dotenv": "^4.4", + "symfony/flex": "^1.1", + "symfony/framework-bundle": "^4.4", + "symfony/monolog-bundle": "^3.3", + "symfony/yaml": "^4.4" + }, + "config": { + "preferred-install": { + "*": "dist" + }, + "sort-packages": true, + "allow-plugins": { + "symfony/flex": true + } + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "replace": { + "paragonie/random_compat": "2.*", + "symfony/polyfill-ctype": "*", + "symfony/polyfill-iconv": "*", + "symfony/polyfill-php71": "*", + "symfony/polyfill-php70": "*", + "symfony/polyfill-php56": "*" + }, + "scripts": { + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd" + }, + "post-install-cmd": [ + "@auto-scripts" + ], + "post-update-cmd": [ + "@auto-scripts" + ], + "test": [ + "@check" + ] + }, + "conflict": { + "symfony/symfony": "*" + }, + "extra": { + "symfony": { + "allow-contrib": false, + "require": "^4.4" + } + } +} diff --git a/CrayFits/config/bootstrap.php b/CrayFits/config/bootstrap.php new file mode 100644 index 00000000..570bb924 --- /dev/null +++ b/CrayFits/config/bootstrap.php @@ -0,0 +1,21 @@ +=1.2) +if (is_array($env = @include dirname(__DIR__).'/.env.local.php')) { + $_ENV += $env; +} elseif (!class_exists(Dotenv::class)) { + throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.'); +} else { + // load all the .env files + (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env'); +} + +$_SERVER += $_ENV; +$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; +$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; +$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; diff --git a/CrayFits/config/bundles.php b/CrayFits/config/bundles.php new file mode 100644 index 00000000..5b11b410 --- /dev/null +++ b/CrayFits/config/bundles.php @@ -0,0 +1,6 @@ + ['all' => true], + Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], +]; diff --git a/CrayFits/config/packages/cache.yaml b/CrayFits/config/packages/cache.yaml new file mode 100644 index 00000000..93e620ef --- /dev/null +++ b/CrayFits/config/packages/cache.yaml @@ -0,0 +1,19 @@ +framework: + cache: + # Put the unique name of your app here: the prefix seed + # is used to compute stable namespaces for cache keys. + #prefix_seed: your_vendor_name/app_name + + # The app cache caches to the filesystem by default. + # Other options include: + + # Redis + #app: cache.adapter.redis + #default_redis_provider: redis://localhost + + # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) + #app: cache.adapter.apcu + + # Namespaced pools use the above "app" backend by default + #pools: + #my.dedicated.cache: ~ diff --git a/CrayFits/config/packages/dev/routing.yaml b/CrayFits/config/packages/dev/routing.yaml new file mode 100644 index 00000000..4116679a --- /dev/null +++ b/CrayFits/config/packages/dev/routing.yaml @@ -0,0 +1,3 @@ +framework: + router: + strict_requirements: true diff --git a/CrayFits/config/packages/framework.yaml b/CrayFits/config/packages/framework.yaml new file mode 100644 index 00000000..d3f884c4 --- /dev/null +++ b/CrayFits/config/packages/framework.yaml @@ -0,0 +1,17 @@ +framework: + secret: '%env(APP_SECRET)%' + #default_locale: en + #csrf_protection: true + #http_method_override: true + + # Enables session support. Note that the session will ONLY be started if you read or write from it. + # Remove or comment this section to explicitly disable session support. + session: + handler_id: ~ + cookie_secure: auto + cookie_samesite: lax + + #esi: true + #fragments: true + php_errors: + log: true diff --git a/CrayFits/config/packages/routing.yaml b/CrayFits/config/packages/routing.yaml new file mode 100644 index 00000000..a15c4ec6 --- /dev/null +++ b/CrayFits/config/packages/routing.yaml @@ -0,0 +1,4 @@ +framework: + router: + strict_requirements: ~ + utf8: true diff --git a/CrayFits/config/packages/test/framework.yaml b/CrayFits/config/packages/test/framework.yaml new file mode 100644 index 00000000..d051c840 --- /dev/null +++ b/CrayFits/config/packages/test/framework.yaml @@ -0,0 +1,4 @@ +framework: + test: true + session: + storage_id: session.storage.mock_file diff --git a/CrayFits/config/packages/test/routing.yaml b/CrayFits/config/packages/test/routing.yaml new file mode 100644 index 00000000..4116679a --- /dev/null +++ b/CrayFits/config/packages/test/routing.yaml @@ -0,0 +1,3 @@ +framework: + router: + strict_requirements: true diff --git a/CrayFits/config/routes.yaml b/CrayFits/config/routes.yaml new file mode 100644 index 00000000..4043e466 --- /dev/null +++ b/CrayFits/config/routes.yaml @@ -0,0 +1,5 @@ +# config/routes.yaml + +app_fits_exec: + path: / + controller: App\Controller\FitsController::generate_fits diff --git a/CrayFits/config/services.yaml b/CrayFits/config/services.yaml new file mode 100644 index 00000000..5c4b4175 --- /dev/null +++ b/CrayFits/config/services.yaml @@ -0,0 +1,27 @@ +# This file is the entry point to configure your own services. +# Files in the packages/ subdirectory configure your dependencies. + +# Put parameters here that don't need to change on each machine where the app is deployed +# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration +parameters: + +services: + # default configuration for services in *this* file + _defaults: + autowire: true # Automatically injects dependencies in your services. + autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + + # makes classes in src/ available to be used as services + # this creates a service per class whose id is the fully-qualified class name + App\: + resource: '../src/*' + exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}' + + # controllers are imported separately to make sure services can be injected + # as action arguments even if you don't extend any base controller class + App\Controller\: + resource: '../src/Controller' + tags: ['controller.service_arguments'] + + # add more service definitions when explicit configuration is needed + # please note that last definitions always *replace* previous ones diff --git a/CrayFits/public/.htaccess b/CrayFits/public/.htaccess new file mode 100644 index 00000000..bcce7fcc --- /dev/null +++ b/CrayFits/public/.htaccess @@ -0,0 +1,68 @@ +# Use the front controller as index file. It serves as a fallback solution when +# every other rewrite/redirect fails (e.g. in an aliased environment without +# mod_rewrite). Additionally, this reduces the matching process for the +# start page (path "/") because otherwise Apache will apply the rewriting rules +# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl). +DirectoryIndex index.php + +# By default, Apache does not evaluate symbolic links if you did not enable this +# feature in your server configuration. Uncomment the following line if you +# install assets as symlinks or if you experience problems related to symlinks +# when compiling LESS/Sass/CoffeScript assets. +# Options FollowSymlinks + +# Disabling MultiViews prevents unwanted negotiation, e.g. "/index" should not resolve +# to the front controller "/index.php" but be rewritten to "/index.php/index". + + Options -MultiViews + + + + RewriteEngine On + + # Determine the RewriteBase automatically and set it as environment variable. + # If you are using Apache aliases to do mass virtual hosting or installed the + # project in a subdirectory, the base path will be prepended to allow proper + # resolution of the index.php file and to redirect to the correct URI. It will + # work in environments without path prefix as well, providing a safe, one-size + # fits all solution. But as you do not need it in this case, you can comment + # the following 2 lines to eliminate the overhead. + RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ + RewriteRule ^(.*) - [E=BASE:%1] + + # Sets the HTTP_AUTHORIZATION header removed by Apache + RewriteCond %{HTTP:Authorization} . + RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Redirect to URI without front controller to prevent duplicate content + # (with and without `/index.php`). Only do this redirect on the initial + # rewrite by Apache and not on subsequent cycles. Otherwise we would get an + # endless redirect loop (request -> rewrite to front controller -> + # redirect -> request -> ...). + # So in case you get a "too many redirects" error or you always get redirected + # to the start page because your Apache does not expose the REDIRECT_STATUS + # environment variable, you have 2 choices: + # - disable this feature by commenting the following 2 lines or + # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the + # following RewriteCond (best solution) + RewriteCond %{ENV:REDIRECT_STATUS} ^$ + RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L] + + # If the requested filename exists, simply serve it. + # We only want to let Apache serve files and not directories. + RewriteCond %{REQUEST_FILENAME} -f + RewriteRule ^ - [L] + + # Rewrite all other queries to the front controller. + RewriteRule ^ %{ENV:BASE}/index.php [L] + + + + + # When mod_rewrite is not available, we instruct a temporary redirect of + # the start page to the front controller explicitly so that the website + # and the generated links can still be used. + RedirectMatch 307 ^/$ /index.php/ + # RedirectTemp cannot be used instead + + diff --git a/CrayFits/public/index.php b/CrayFits/public/index.php new file mode 100644 index 00000000..e30f90c0 --- /dev/null +++ b/CrayFits/public/index.php @@ -0,0 +1,27 @@ +handle($request); +$response->send(); +$kernel->terminate($request, $response); diff --git a/CrayFits/src/Controller/.gitignore b/CrayFits/src/Controller/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/CrayFits/src/Controller/FitsController.php b/CrayFits/src/Controller/FitsController.php new file mode 100644 index 00000000..f6d85416 --- /dev/null +++ b/CrayFits/src/Controller/FitsController.php @@ -0,0 +1,75 @@ + $_ENV['FITS_WEBSERVICE_URI']]; + $this->client = new Client($options); + } + + /** + * @param Request $request + * @param LoggerInterface $loggerger + * @return StreamedResponse | Response; + */ + public function generate_fits(Request $request) { + set_time_limit(0); + $logger = new Logger('islandora_fits'); + $logger->pushHandler(new StreamHandler('/var/log/islandora/fits.log', Logger::DEBUG)); + $token = $request->headers->get('Authorization'); + $file_uri = $request->headers->get('Apix-Ldp-Resource'); + // If no file has been passed it probably because someone is testing the url from their browser. + if (!$file_uri) { + return new Response("

The Fits microservice is up and running.

"); + } + $context = stream_context_create([ + "http" => [ + "header" => "Authorization: $token", + ], + ]); + try { + $response = $this->client->post('examine', [ + 'multipart' => [ + [ + 'name' => 'datafile', + 'filename' => $file_uri, + 'contents' => fopen($file_uri, 'r', FALSE, $context), + ], + ], + ]); + } + catch (\Exception $e) { + $logger->addError('ERROR', [$e->getMessage()]); + } + $logger->addInfo('Response Status', ["Status" => $response->getStatusCode(), "URI" => $file_uri]); + $fits_xml = $response->getBody()->getContents(); + $encoding = mb_detect_encoding($fits_xml, 'UTF-8', TRUE); + if ($encoding != 'UTF-8') { + $fits_xml = utf8_encode($fits_xml); + } + $response = new StreamedResponse(); + $response->headers->set('Content-Type', 'application/xml'); + $response->setCallback(function () use ($fits_xml) { + echo($fits_xml); + }); + return $response; + } +} diff --git a/CrayFits/src/Kernel.php b/CrayFits/src/Kernel.php new file mode 100644 index 00000000..785b0bef --- /dev/null +++ b/CrayFits/src/Kernel.php @@ -0,0 +1,53 @@ +getProjectDir().'/config/bundles.php'; + foreach ($contents as $class => $envs) { + if ($envs[$this->environment] ?? $envs['all'] ?? false) { + yield new $class(); + } + } + } + + public function getProjectDir(): string + { + return \dirname(__DIR__); + } + + protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void + { + $container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php')); + $container->setParameter('container.dumper.inline_class_loader', true); + $confDir = $this->getProjectDir().'/config'; + + $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob'); + $loader->load($confDir.'/{packages}/'.$this->environment.'/**/*'.self::CONFIG_EXTS, 'glob'); + $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob'); + $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob'); + } + + protected function configureRoutes(RouteCollectionBuilder $routes): void + { + $confDir = $this->getProjectDir().'/config'; + + $routes->import($confDir.'/{routes}/'.$this->environment.'/**/*'.self::CONFIG_EXTS, '/', 'glob'); + $routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob'); + $routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob'); + } +} diff --git a/CrayFits/src/Service/FitsGenerator.php b/CrayFits/src/Service/FitsGenerator.php new file mode 100644 index 00000000..8d6368d1 --- /dev/null +++ b/CrayFits/src/Service/FitsGenerator.php @@ -0,0 +1,36 @@ + $_ENV['FITS_WEBSERVICE_URI'], + ]; + + $client = new Client($options); + $response = $client->post('examine', [ + 'multipart' => [ + [ + 'name' => 'datafile', + 'filename' => $input_filename, + 'contents' => file_get_contents($input_filename), + ], + ] + ]); + return $response->getBody()->getContents(); + } +} diff --git a/CrayFits/symfony.lock b/CrayFits/symfony.lock new file mode 100644 index 00000000..9d60ffd4 --- /dev/null +++ b/CrayFits/symfony.lock @@ -0,0 +1,159 @@ +{ + "guzzlehttp/guzzle": { + "version": "6.3.3" + }, + "guzzlehttp/promises": { + "version": "v1.3.1" + }, + "guzzlehttp/psr7": { + "version": "1.5.2" + }, + "monolog/monolog": { + "version": "1.24.0" + }, + "psr/cache": { + "version": "1.0.1" + }, + "psr/container": { + "version": "1.0.0" + }, + "psr/http-message": { + "version": "1.0.1" + }, + "psr/log": { + "version": "1.1.0" + }, + "psr/simple-cache": { + "version": "1.0.1" + }, + "ralouphie/getallheaders": { + "version": "2.0.5" + }, + "symfony/cache": { + "version": "v4.2.7" + }, + "symfony/config": { + "version": "v4.2.7" + }, + "symfony/console": { + "version": "3.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "3.3", + "ref": "482d233eb8de91ebd042992077bbd5838858890c" + }, + "files": [ + "bin/console", + "config/bootstrap.php" + ] + }, + "symfony/contracts": { + "version": "v1.0.2" + }, + "symfony/debug": { + "version": "v4.2.7" + }, + "symfony/dependency-injection": { + "version": "v4.2.7" + }, + "symfony/dotenv": { + "version": "v4.2.7" + }, + "symfony/event-dispatcher": { + "version": "v4.2.7" + }, + "symfony/filesystem": { + "version": "v4.2.7" + }, + "symfony/finder": { + "version": "v4.2.7" + }, + "symfony/flex": { + "version": "1.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "1.0", + "ref": "dc3fc2e0334a4137c47cfd5a3ececc601fa61a0b" + }, + "files": [ + ".env" + ] + }, + "symfony/framework-bundle": { + "version": "4.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.2", + "ref": "f64037a414de7d861f68e9b5b5c0e4f7425e2002" + }, + "files": [ + "config/bootstrap.php", + "config/packages/cache.yaml", + "config/packages/framework.yaml", + "config/packages/test/framework.yaml", + "config/services.yaml", + "public/index.php", + "src/Controller/.gitignore", + "src/Kernel.php" + ] + }, + "symfony/http-foundation": { + "version": "v4.2.7" + }, + "symfony/http-kernel": { + "version": "v4.2.7" + }, + "symfony/monolog-bridge": { + "version": "v4.2.8" + }, + "symfony/monolog-bundle": { + "version": "3.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "3.3", + "ref": "6240c6d43e8237a32452f057f81816820fd56ab6" + }, + "files": [ + "config/packages/dev/monolog.yaml", + "config/packages/prod/monolog.yaml", + "config/packages/test/monolog.yaml" + ] + }, + "symfony/polyfill-intl-idn": { + "version": "v1.20.0" + }, + "symfony/polyfill-intl-normalizer": { + "version": "v1.20.0" + }, + "symfony/polyfill-mbstring": { + "version": "v1.11.0" + }, + "symfony/polyfill-php72": { + "version": "v1.20.0" + }, + "symfony/routing": { + "version": "4.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.2", + "ref": "5374e24d508ba8fd6ba9eb15170255fdb778316a" + }, + "files": [ + "config/packages/dev/routing.yaml", + "config/packages/routing.yaml", + "config/packages/test/routing.yaml", + "config/routes.yaml" + ] + }, + "symfony/var-exporter": { + "version": "v4.2.7" + }, + "symfony/yaml": { + "version": "v4.2.7" + } +} diff --git a/README.md b/README.md index e6fd4fe6..7d8a7ef7 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Many microservices have extra installation requirements. Please see the README Crayfish contains the following services +* [CrayFits](./CrayFits): FITS as a microservice. * [Homarus](./Homarus): FFmpeg as a microservice. * [Houdini](./Houdini): ImageMagick as a microservice. * [Hypercube](./Hypercube): Tesseract as a microservice.