Завершено прикусом маркера - создание отдельной таблицы для файлов вместе со своим собственным API с использованием грозный и node-postgres .Если это кому-то поможет, вот как это получилось.
Определение данных PostgreSQL ...
-- DROP SEQUENCE public.files_seq;
CREATE SEQUENCE IF NOT EXISTS public.files_seq;
-- DROP TABLE public.files;
CREATE TABLE IF NOT EXISTS public.files (
_id integer PRIMARY KEY DEFAULT nextval('files_seq'::regclass),
name character varying(512) NOT NULL,
type character varying(20) NOT NULL,
data bytea
);
-- DROP INDEX public.users_first_name;
CREATE INDEX files_name ON public.files USING btree (name);
Контроллер для Express ...
import stream from 'stream';
import fs from 'fs';
import { IncomingForm } from 'formidable';
import db from '../../utils/db';
// Returns list of images
export async function index(req, res) {
const { rows } = await db.query('SELECT _id, name, type FROM files ORDER BY name;', []);
return res.send(rows);
}
// Uploads a single file
export async function upload(req, res) {
let _id;
new IncomingForm().parse(req, (err, fields, files) => {
if(err) throw err;
if(Array.isArray(files)) throw new Error('Only one file can be uploaded at a time');
const { name, type, path } = files.file;
fs.readFile(path, 'hex', async(err, fileData) => {
if(err) throw err;
fileData = `\\x${fileData}`;
const sql = 'INSERT INTO files (name, type, data) VALUES($1, $2, $3) RETURNING _id;';
const { rows } = await db.query(sql, [name, type, fileData]);
_id = rows[0]._id;
res.send({ id: _id });
// console.log(`Uploaded ${name} to ${path} and inserted into database (ID = ${_id})`);
// No need to delete the file uploaded as Heroku has an ephemeral file system
});
});
}
// Downloads a file by its _id
export async function download(req, res) {
const _id = req.params.id;
const sql = 'SELECT _id, name, type, data FROM files WHERE _id = $1;';
const { rows } = await db.query(sql, [_id]);
const file = rows[0];
const fileContents = Buffer.from(file.data, 'base64');
const readStream = new stream.PassThrough();
readStream.end(fileContents);
res.set('Content-disposition', `attachment; filename=${file.name}`);
res.set('Content-Type', file.type);
readStream.pipe(res);
return rows[0];
}
// Deletes a file from the database (admin-only)
export async function destroy(req, res) {
const _id = req.params.id;
const sql = 'DELETE FROM files WHERE _id = $1;';
await db.query(sql, [_id]);
res.status(204).send({ message: `File ${_id} deleted.`});
}
Вкл.на стороне клиента я использую ng-file-upload с AngularJS.
Вот соответствующая часть представления (для краткости pug) ...
.form-group.col-md-6
label(for='photo') Teacher photo
input.form-control(ngf-select='$ctrl.uploadPhoto($file)', type='file', id='photo', name='photo', ng-model='$ctrl.user.photo', ngf-pattern="'image/*'", ngf-accept="'image/*'", ngf-max-size='100KB', ngf-min-height='276', ngf-max-height='276', ngf-min-width='236', ngf-max-width='236', ngf-resize='{width: 236, height: 276}', ngf-model-invalid='errorFile')
ng-messages.help-block.has-error(for='form.photo.$error', ng-show='form.photo.$dirty || form.$submitted', role='alert')
ng-message(when='maxSize') Please select a photo that is less than 100K.
ng-message(when='minHeight,maxHeight,minWidth,maxWidth') The image must be 236 x 276 pixels.
span(ng-if='$ctrl.user.imageId')
img(ng-src='/api/file/{{ $ctrl.user.imageId }}' alt="Photo of Teacher")
и метод в его контроллере ...
uploadPhoto(file) {
if(file) {
this.uploadService.upload({
url: '/api/file/upload',
data: { file }
})
.then(response => {
this.user.imageId = response.data.id;
}, response => {
if(response.status > 0) console.log(`${response.status}: ${response.data}`);
}, evt => {
// Math.min is to fix IE which reports 200% sometimes
this.uploadProgress = Math.min(100, parseInt(100.0 * evt.loaded / evt.total, 10));
});
}
}