Skip to content

Commit

Permalink
Merge pull request #43 from creative-commoners/pulls/2.0/update-files…
Browse files Browse the repository at this point in the history
…ystem-handling

API Import and feed generation use abstract filesystem, abstract writes to memory
  • Loading branch information
NightJar committed Nov 27, 2017
2 parents c4c649e + 1553251 commit 533f136
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 72 deletions.
12 changes: 0 additions & 12 deletions _config.php

This file was deleted.

8 changes: 8 additions & 0 deletions _config/filesystem.yml
@@ -0,0 +1,8 @@
---
Name: registryfilesystem
---
SilverStripe\Core\Injector\Injector:
SilverStripe\Registry\RegistryImportFeed:
properties:
AssetHandler: '%$SilverStripe\Assets\Storage\GeneratedAssetHandler'
AssetsDir: '`ASSETS_DIR`'
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -19,7 +19,8 @@
],
"require": {
"silverstripe/cms": "^4.0",
"silverstripe/admin": "^1.0"
"silverstripe/admin": "^1.0",
"silverstripe/assets": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7",
Expand Down
35 changes: 15 additions & 20 deletions src/RegistryAdmin.php
Expand Up @@ -52,19 +52,16 @@ public function getExportFields()
return $fields;
}

public function getCsvImportsPath()
/**
* Gets a unique filename to use for importing the uploaded CSV data
*
* @return string
*/
public function getCsvImportFilename()
{
$base = REGISTRY_IMPORT_PATH;
if (!file_exists($base)) {
mkdir($base);
}
$feed = RegistryImportFeed::singleton();

$path = sprintf('%s/%s', $base, $this->sanitiseClassName($this->modelClass));
if (!file_exists($path)) {
mkdir($path);
}

return $path;
return sprintf('%s/%s', $feed->getStoragePath($this->modelClass), $feed->getImportFilename());
}

public function import($data, $form, $request)
Expand All @@ -78,13 +75,12 @@ public function import($data, $form, $request)
$importers = $this->getModelImporters();
$loader = $importers[$this->modelClass];

$fileContents = !empty($data['_CsvFile']['tmp_name']) ? file_get_contents($data['_CsvFile']['tmp_name']) : '';
// File wasn't properly uploaded, show a reminder to the user
if (empty($_FILES['_CsvFile']['tmp_name'])
|| file_get_contents($_FILES['_CsvFile']['tmp_name']) == ''
) {
if (!$fileContents) {
$form->sessionMessage(
_t('SilverStripe\\Admin\\ModelAdmin.NOCSVFILE', 'Please browse for a CSV file to import'),
'good'
'bad'
);
$this->redirectBack();
return false;
Expand All @@ -94,13 +90,12 @@ public function import($data, $form, $request)
$loader->deleteExistingRecords = true;
}

$results = $loader->load($_FILES['_CsvFile']['tmp_name']);
$results = $loader->load($data['_CsvFile']['tmp_name']);

// copy the uploaded file into the export path
copy(
$_FILES['_CsvFile']['tmp_name'],
sprintf('%s/import-%s.csv', $this->getCsvImportsPath(), date('Y-m-dHis'))
);
RegistryImportFeed::singleton()
->getAssetHandler()
->setContent($this->getCsvImportFilename(), $fileContents);

$message = '';
if ($results->CreatedCount()) {
Expand Down
156 changes: 137 additions & 19 deletions src/RegistryImportFeed.php
Expand Up @@ -2,17 +2,52 @@

namespace SilverStripe\Registry;

use League\Flysystem\Plugin\ListFiles;
use SilverStripe\Assets\Storage\GeneratedAssetHandler;
use SilverStripe\Control\RSS\RSSFeed;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBDatetime;

class RegistryImportFeed
{
use Configurable;
use Injectable;

/**
* The path format to store imported record files in (inside the assets directory)
*
* @config
* @var string
*/
private static $storage_path = '_imports/{model}';

/**
* The filename to use for storing imported record files. Used by RegistryImportFeedController to save files to.
*
* @config
* @var string
*/
private static $storage_filename = 'import-{date}.csv';

protected $modelClass;

/**
* The class used to manipulate imported feed files on the filesystem
*
* @var GeneratedAssetHandler
*/
protected $assetHandler;

/**
* The "assets" folder name
*
* @var string
*/
protected $assetsDir;

public function setModelClass($class)
{
$this->modelClass = $class;
Expand All @@ -21,25 +56,12 @@ public function setModelClass($class)

public function getLatest()
{
$files = ArrayList::create();

$path = REGISTRY_IMPORT_PATH . '/' . $this->sanitiseClassName($this->modelClass);
if (file_exists($path)) {
$registryPage = DataObject::get_one(
RegistryPage::class,
['DataClass' => $this->modelClass]
);

if ($registryPage && $registryPage->exists()) {
foreach (array_diff(scandir($path), array('.', '..')) as $file) {
$files->push(RegistryImportFeedEntry::create(
$file,
'',
filemtime($path . '/' . $file),
REGISTRY_IMPORT_URL . '/' . $this->sanitiseClassName($this->modelClass) . '/' . $file
));
}
}
$registryPage = RegistryPage::get()->filter(['DataClass' => $this->modelClass])->first();
if ($registryPage && $registryPage->exists()) {
$files = $this->getImportFiles();
} else {
// Always return an empty list of the model isn't associated to any RegistryPages
$files = ArrayList::create();
}

return RSSFeed::create(
Expand All @@ -49,6 +71,102 @@ public function getLatest()
);
}

/**
* Set the handler used to manipulate the filesystem, and add the ListFiles plugin from Flysystem to inspect
* the contents of a directory
*
* @param GeneratedAssetHandler $handler
* @return $this
*/
public function setAssetHandler(GeneratedAssetHandler $handler)
{
$handler->getFilesystem()->addPlugin(new ListFiles);

$this->assetHandler = $handler;

return $this;
}

/**
* Get the handler used to manipulate the filesystem
*
* @return GeneratedAssetHandler
*/
public function getAssetHandler()
{
return $this->assetHandler;
}

/**
* Get the path that import files will be stored for this model
*
* @param string $modelClass If null, the current model class will be used
* @return string
*/
public function getStoragePath($modelClass = null)
{
$sanitisedClassName = $this->sanitiseClassName($modelClass ?: $this->modelClass);
return str_replace('{model}', $sanitisedClassName, $this->config()->get('storage_path'));
}

/**
* Loop import files in the storage path and push them into an {@link ArrayList}
*
* @return ArrayList
*/
public function getImportFiles()
{
$path = $this->getStoragePath();
$importFiles = $this->getAssetHandler()->getFilesystem()->listFiles($path);

$files = ArrayList::create();

foreach ($importFiles as $importFile) {
$files->push(RegistryImportFeedEntry::create(
$importFile['basename'],
'',
DBDatetime::create()->setValue($importFile['timestamp'])->Format(DBDatetime::ISO_DATETIME),
$this->getAssetsDir() . '/' . $importFile['path']
));
}

return $files;
}

/**
* Returns a relatively unique filename to storage imported data feeds as
*
* @return string
*/
public function getImportFilename()
{
// Note: CLDR date format see DBDatetime
$datetime = DBDatetime::now()->Format('y-MM-dd-HHmmss');
return str_replace('{date}', $datetime, $this->config()->get('storage_filename'));
}

/**
* Set the assets directory name
*
* @param string $assetsDir
* @return $this
*/
public function setAssetsDir($assetsDir)
{
$this->assetsDir = $assetsDir;
return $this;
}

/**
* Get the assets directory name
*
* @return string
*/
public function getAssetsDir()
{
return $this->assetsDir;
}

/**
* See {@link \SilverStripe\Admin\ModelAdmin::sanitiseClassName}
*
Expand Down
36 changes: 17 additions & 19 deletions src/RegistryPageController.php
Expand Up @@ -230,51 +230,49 @@ public function export($request)
$dataClass = $this->dataRecord->getDataClass();
$resultColumns = $this->dataRecord->getDataSingleton()->fieldLabels();

if (!file_exists(REGISTRY_EXPORT_PATH)) {
mkdir(REGISTRY_EXPORT_PATH);
}
$base = REGISTRY_EXPORT_PATH . '/' . $dataClass;
if (!file_exists($base)) {
mkdir($base);
}
// Used for the browser, not stored on the server
$filepath = sprintf('export-%s.csv', date('Y-m-dHis'));

$filepath = sprintf('%s/export-%s.csv', $base, date('Y-m-dHis'));
$file = fopen($filepath, 'w');
// Allocates up to 1M of memory storage to write to, then will fail over to a temporary file on the filesystem
$handle = fopen('php://temp/maxmemory:' . (1024 * 1024), 'w');

$cols = array_keys($resultColumns);

// put the headers in the first row
fputcsv($file, $cols, ',', '"');
fputcsv($handle, $cols);

// put the data in the rows after
foreach ($this->RegistryEntries(false) as $result) {
$item = array();
$item = [];
foreach ($cols as $col) {
$item[] = $result->$col;
}
fputcsv($file, $item, ',', '"');
fputcsv($handle, $item);
}

fclose($file);
rewind($handle);

// if the headers can't be sent (i.e. running a unit test, or something)
// just return the file path so the user can manually download the csv
if (!headers_sent() && $this->config()->get('output_headers')) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=' . basename($filepath));
header('Content-Disposition: attachment; filename=' . $filepath);
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . filesize($filepath));
header('Content-Length: ' . fstat($handle)['size']);
ob_clean();
flush();
readfile($filepath);

unlink($filepath);
echo stream_get_contents($handle);

fclose($handle);
} else {
$contents = file_get_contents($filepath);
unlink($filepath);
$contents = stream_get_contents($handle);
fclose($handle);

return $contents;
}
}
Expand Down
2 changes: 1 addition & 1 deletion templates/SilverStripe/Registry/Layout/RegistryPage.ss
Expand Up @@ -30,7 +30,7 @@ $Content
</table>

<div class="resultActions">
<a class="export" href="$Link(export)?$AllQueryVars" title="<%t SilverStripe\\Registry\\RegistryPage.ExportAllTitle "Export all results to a CSV spreadsheet file" %>">
<a class="export" href="$Link(export)?$AllQueryVars.RAW" title="<%t SilverStripe\\Registry\\RegistryPage.ExportAllTitle "Export all results to a CSV spreadsheet file" %>">
<%t SilverStripe\\Registry\\RegistryPage.ExportAll "Export results to CSV" %>
</a>
</div>
Expand Down
24 changes: 24 additions & 0 deletions tests/RegistryImportFeedTest.php
@@ -0,0 +1,24 @@
<?php

namespace SilverStripe\Registry\Tests;

use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\Registry\RegistryImportFeed;

class RegistryImportFeedTest extends SapphireTest
{
public function testGetStoragePath()
{
$importFeed = RegistryImportFeed::create();
$this->assertSame('_imports/Foo-Bar-ModelName', $importFeed->getStoragePath('Foo\Bar\ModelName'));
}

public function testGetImportFilename()
{
DBDatetime::set_mock_now('2017-01-01 12:30:45');

$importFeed = RegistryImportFeed::create();
$this->assertContains('import-2017-01-01', $importFeed->getImportFilename());
}
}

0 comments on commit 533f136

Please sign in to comment.