Skip to content

Commit

Permalink
feat: implement direct file upload
Browse files Browse the repository at this point in the history
Signed-off-by: Luka Trovic <luka@nextcloud.com>
  • Loading branch information
luka-nextcloud committed Feb 15, 2024
1 parent 159d2ec commit ea126c7
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 10 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
14 changes: 14 additions & 0 deletions cypress/e2e/tables-import.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,18 @@ describe('Import csv', () => {
cy.get('[data-cy="importResultRowErrors"]').should('contain.text', '0')
})

it('Import csv with upload file button', () => {
cy.loadTable('Tutorial')
cy.clickOnTableThreeDotMenu('Import')
cy.get('.modal__content button').contains('Upload a file').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('contain.text', '0')
cy.get('[data-cy="importResultRowErrors"]').should('contain.text', '0')
})

})
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;
}
}
4 changes: 4 additions & 0 deletions lib/Service/ImportService.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ 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 {
}
103 changes: 94 additions & 9 deletions src/modules/modals/Import.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,23 @@
</template>
{{ t('tables', 'Select a file') }}
</NcButton>
<input v-model="path" :class="{ missing: pathError }">
<input v-model="path" :class="{ missing: pathError }" :disabled="!!selectedUploadFile">
</div>

<div class="fix-col-4 space-T-small middle">
<NcButton :aria-label="t('tables', 'Upload a file')" @click="selectUploadFile">
<template #icon>
<IconUpload :size="20" />
</template>
{{ t('tables', 'Upload a file') }}
</NcButton>
<input ref="uploadFileInput"
type="file"
aria-hidden="true"
class="hidden-visually"
:accept="mimeTypes.join(',')"
@change="onUploadFileInputChange">
<input :value="selectedUploadFile ? selectedUploadFile.name : ''" disabled>
</div>
</RowFormWrapper>

Expand Down Expand Up @@ -137,6 +153,7 @@ import { FilePicker, FilePickerType, showError, showWarning } from '@nextcloud/d
import RowFormWrapper from '../../shared/components/ncTable/partials/rowTypePartials/RowFormWrapper.vue'
import permissionsMixin from '../../shared/components/ncTable/mixins/permissionsMixin.js'
import IconFolder from 'vue-material-design-icons/Folder.vue'
import IconUpload from 'vue-material-design-icons/Upload.vue'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { mapGetters } from 'vuex'
Expand All @@ -148,6 +165,7 @@ export default {
NcIconTimerSand,
NcLoadingIcon,
IconFolder,
IconUpload,
NcModal,
NcButton,
NcCheckboxRadioSwitch,
Expand Down Expand Up @@ -180,6 +198,15 @@ export default {
loading: false,
result: null,
waitForReload: false,
mimeTypes: [
'text/csv',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/xml',
'text/html',
'application/vnd.oasis.opendocument.spreadsheet',
],
selectedUploadFile: null,
}
},
Expand All @@ -198,6 +225,16 @@ export default {
this.createMissingColumns = this.canCreateMissingColumns
}
},
path(val) {
if (val !== '') {
this.clearSelectedUploadFile()
}
},
selectedUploadFile(val) {
if (val !== null) {
this.path = ''
}
},
},
methods: {
Expand Down Expand Up @@ -226,6 +263,16 @@ export default {
this.actionCancel()
},
actionSubmit() {
if (this.selectedUploadFile && this.selectedUploadFile.type !== '' && !this.mimeTypes.includes(this.selectedUploadFile.type)) {
showWarning(t('tables', 'The selected file is not supported.'))
return null
}
if (this.selectedUploadFile) {
this.uploadFile()
return
}
if (this.path === '') {
showWarning(t('tables', 'Please select a file.'))
this.pathError = true
Expand Down Expand Up @@ -259,6 +306,41 @@ export default {
return false
}
},
async uploadFile() {
this.loading = true
try {
const url = generateUrl('/apps/tables/importupload/' + (this.isElementView ? 'view' : 'table') + '/' + this.element.id)
const formData = new FormData()
formData.append('uploadfile', this.selectedUploadFile)
formData.append('createMissingColumns', this.getCreateMissingColumns)
const res = await axios.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
if (res.status === 200) {
this.result = res.data
this.loading = false
} else if (res.status === 401) {
console.debug('error while importing', res)
showError(t('tables', 'Could not import, not authorized. Are you logged in?'))
} else if (res.status === 403) {
console.debug('error while importing', res)
showError(t('tables', 'Could not import, missing needed permission.'))
} else if (res.status === 404) {
console.debug('error while importing', res)
showError(t('tables', 'Could not import, needed resources were not found.'))
} else {
showError(t('tables', 'Could not import data due to unknown errors.'))
console.debug('error while importing', res)
}
} catch (e) {
console.error(e)
return false
}
},
actionCancel() {
this.reset()
this.$emit('close')
Expand All @@ -274,14 +356,7 @@ export default {
const filePicker = new FilePicker(
t('text', 'Select file for the import'),
false, // multiselect
[
'text/csv',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/xml',
'text/html',
'application/vnd.oasis.opendocument.spreadsheet',
], // mime filter
this.mimeTypes, // mime filter
true, // modal
FilePickerType.Choose, // type
false, // directories
Expand All @@ -295,6 +370,16 @@ export default {
})
})
},
selectUploadFile() {
this.$refs.uploadFileInput.click()
},
clearSelectedUploadFile() {
this.selectedUploadFile = null
this.$refs.uploadFileInput.value = ''
},
onUploadFileInputChange(event) {
this.selectedUploadFile = event.target.files[0]
},
},
}
Expand Down

0 comments on commit ea126c7

Please sign in to comment.