Skip to content

Commit

Permalink
Merge pull request #845 from nextcloud/feature/import-table-with-uplo…
Browse files Browse the repository at this point in the history
…ad-file

feat: implement direct file upload
  • Loading branch information
juliusknorr authored Feb 28, 2024
2 parents 6c1220d + 207c0a8 commit 9558791
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 54 deletions.
2 changes: 2 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@
// import
['name' => 'import#importInTable', 'url' => '/import/table/{tableId}', 'verb' => 'POST'],
['name' => 'import#importInView', 'url' => '/import/view/{viewId}', 'verb' => 'POST'],
['name' => 'import#importUploadInTable', 'url' => '/importupload/table/{tableId}', 'verb' => 'POST'],
['name' => 'import#importUploadInView', 'url' => '/importupload/view/{viewId}', 'verb' => 'POST'],

// search
['name' => 'search#all', 'url' => '/search/all', 'verb' => 'GET'],
Expand Down
22 changes: 18 additions & 4 deletions cypress/e2e/tables-import.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,34 @@ describe('Import csv', () => {
cy.visit('apps/tables')
})

it('Import csv', () => {
it('Import csv from Files', () => {
cy.uploadFile('test-import.csv', 'text/csv')
cy.loadTable('Tutorial')
cy.clickOnTableThreeDotMenu('Import')
cy.get('.modal__content button').contains('Select a file').click()
cy.get('.modal__content button').contains('Select from Files').click()
cy.get('.file-picker__files').contains('test-import').click()
cy.get('.file-picker button span').contains('Choose test-import.csv').click()
cy.get('.modal__content button').contains('Import').click()
cy.get('[data-cy="importResultColumnsFound"]').should('contain.text', '4')
cy.get('[data-cy="importResultColumnsMatch"]').should('contain.text', '4')
cy.get('[data-cy="importResultColumnsCreated"]').should('contain.text', '0')
cy.get('[data-cy="importResultRowsInserted"]').should('contain.text', '3')
cy.get('[data-cy="importResultParsingErrors"]').should('contain.text', '0')
cy.get('[data-cy="importResultRowErrors"]').should('contain.text', '0')
cy.get('[data-cy="importResultParsingErrors"]').should('not.exist')
cy.get('[data-cy="importResultRowErrors"]').should('not.exist')
})

it('Import csv from device', () => {
cy.loadTable('Tutorial')
cy.clickOnTableThreeDotMenu('Import')
cy.get('.modal__content button').contains('Upload from device').click()
cy.get('input[type="file"]').selectFile('cypress/fixtures/test-import.csv', { force: true })
cy.get('.modal__content button').contains('Import').click()
cy.get('[data-cy="importResultColumnsFound"]').should('contain.text', '4')
cy.get('[data-cy="importResultColumnsMatch"]').should('contain.text', '4')
cy.get('[data-cy="importResultColumnsCreated"]').should('contain.text', '0')
cy.get('[data-cy="importResultRowsInserted"]').should('contain.text', '3')
cy.get('[data-cy="importResultParsingErrors"]').should('not.exist')
cy.get('[data-cy="importResultRowErrors"]').should('not.exist')
})

})
100 changes: 99 additions & 1 deletion lib/Controller/ImportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,47 @@

use OCA\Tables\AppInfo\Application;
use OCA\Tables\Service\ImportService;
use OCA\Tables\UploadException;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\Files\NotPermittedException;
use OCP\IL10N;
use OCP\IRequest;
use OCP\Util;
use Psr\Log\LoggerInterface;

class ImportController extends Controller {
public const MIME_TYPES = [
'text/csv',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/xml',
'text/html',
'application/vnd.oasis.opendocument.spreadsheet',
];

private ImportService $service;
private string $userId;

protected LoggerInterface $logger;

private IL10N $l10n;

use Errors;


public function __construct(
IRequest $request,
LoggerInterface $logger,
ImportService $service,
string $userId) {
string $userId,
IL10N $l10n) {
parent::__construct(Application::APP_ID, $request);
$this->logger = $logger;
$this->service = $service;
$this->userId = $userId;
$this->l10n = $l10n;
}


Expand All @@ -48,4 +65,85 @@ public function importInView(int $viewId, String $path, bool $createMissingColum
return $this->service->import(null, $viewId, $path, $createMissingColumns);
});
}

/**
* @NoAdminRequired
*/
public function importUploadInTable(int $tableId, bool $createMissingColumns = true): DataResponse {
try {
$file = $this->getUploadedFile('uploadfile');
return $this->handleError(function () use ($tableId, $file, $createMissingColumns) {
return $this->service->import($tableId, null, $file['tmp_name'], $createMissingColumns);
});
} catch (UploadException | NotPermittedException $e) {
$this->logger->error('Upload error', ['exception' => $e]);
return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
}
}

/**
* @NoAdminRequired
*/
public function importUploadInView(int $viewId, bool $createMissingColumns = true): DataResponse {
try {
$file = $this->getUploadedFile('uploadfile');
return $this->handleError(function () use ($viewId, $file, $createMissingColumns) {
return $this->service->import(null, $viewId, $file['tmp_name'], $createMissingColumns);
});
} catch (UploadException | NotPermittedException $e) {
$this->logger->error('Upload error', ['exception' => $e]);
return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
}
}

/**
* @param string $key
* @return array
* @throws UploadException
*/
private function getUploadedFile(string $key): array {
$file = $this->request->getUploadedFile($key);
$phpFileUploadErrors = [
UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
];

if (empty($file)) {
throw new UploadException($this->l10n->t('No file uploaded or file size exceeds maximum of %s', [Util::humanFileSize(Util::uploadLimit())]));
}

if (array_key_exists('error', $file) && $file['error'] !== UPLOAD_ERR_OK) {
throw new UploadException($phpFileUploadErrors[$file['error']]);
}

if (isset($file['tmp_name'], $file['name'], $file['type'])) {
$fileType = $file['type'];

if (function_exists('mime_content_type')) {
$fileType = @mime_content_type($file['tmp_name']);
}

if (!$fileType) {
$fileType = $file['type'];
}

if (!in_array($fileType, self::MIME_TYPES, true)) {
throw new UploadException('File type not supported: ' . $fileType);
}

$newFileResource = fopen($file['tmp_name'], 'rb');

if ($newFileResource === false) {
throw new UploadException('Could not read file');
}
}

return $file;
}
}
3 changes: 3 additions & 0 deletions lib/Service/ImportService.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ public function import(?int $tableId, ?int $viewId, string $path, bool $createMi
} else {
$error = true;
}
} elseif (\file_exists($path)) {
$spreadsheet = IOFactory::load($path);
$this->loop($spreadsheet->getActiveSheet());
} else {
$error = true;
}
Expand Down
6 changes: 6 additions & 0 deletions lib/UploadException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php

namespace OCA\Tables;

class UploadException extends \Exception {
}
Loading

0 comments on commit 9558791

Please sign in to comment.