Autosave: 20260405-182715
This commit is contained in:
parent
d3ec77b828
commit
68e6ac5ac1
@ -1,26 +1,23 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const config = require('../config');
|
|
||||||
const path = require('path');
|
|
||||||
const passport = require('passport');
|
const passport = require('passport');
|
||||||
const services = require('../services/file');
|
const services = require('../services/file');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get('/download', (req, res) => {
|
router.get('/download', (req, res) => {
|
||||||
if (process.env.NODE_ENV == "production" || process.env.NEXT_PUBLIC_BACK_API) {
|
if (process.env.NODE_ENV == 'production' || process.env.NEXT_PUBLIC_BACK_API) {
|
||||||
services.downloadGCloud(req, res);
|
services.downloadGCloud(req, res);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
services.downloadLocal(req, res);
|
services.downloadLocal(req, res);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/upload/:table/:field', passport.authenticate('jwt', {session: false}), (req, res) => {
|
router.post('/upload/:table/:field', passport.authenticate('jwt', { session: false }), (req, res) => {
|
||||||
const fileName = `${req.params.table}/${req.params.field}`;
|
const fileName = `${req.params.table}/${req.params.field}`;
|
||||||
|
|
||||||
if (process.env.NODE_ENV == "production" || process.env.NEXT_PUBLIC_BACK_API) {
|
if (process.env.NODE_ENV == 'production' || process.env.NEXT_PUBLIC_BACK_API) {
|
||||||
services.uploadGCloud(fileName, req, res);
|
services.uploadGCloud(fileName, req, res);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
services.uploadLocal(fileName, {
|
services.uploadLocal(fileName, {
|
||||||
entity: null,
|
entity: null,
|
||||||
maxFileSize: 10 * 1024 * 1024,
|
maxFileSize: 10 * 1024 * 1024,
|
||||||
@ -29,4 +26,39 @@ router.post('/upload/:table/:field', passport.authenticate('jwt', {session: fals
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/upload-public/:table/:field', (req, res) => {
|
||||||
|
const fileName = `${req.params.table}/${req.params.field}`;
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV == 'production' || process.env.NEXT_PUBLIC_BACK_API) {
|
||||||
|
services.uploadGCloud(fileName, req, res);
|
||||||
|
} else {
|
||||||
|
services.uploadLocal(fileName, {
|
||||||
|
entity: null,
|
||||||
|
maxFileSize: 50 * 1024 * 1024,
|
||||||
|
folderIncludesAuthenticationUid: false,
|
||||||
|
allowAnonymous: true,
|
||||||
|
allowedMimePrefixes: ['audio/', 'video/'],
|
||||||
|
})(req, res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/fetch-public-playlist', express.json({ limit: '100kb' }), async (req, res) => {
|
||||||
|
const playlistUrl = typeof req.body?.url === 'string' ? req.body.url.trim() : '';
|
||||||
|
|
||||||
|
if (!playlistUrl) {
|
||||||
|
res.status(400).send({ message: 'A playlist URL is required.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await services.fetchPublicTextFile(playlistUrl);
|
||||||
|
res.status(200).send(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch public playlist URL:', { url: playlistUrl, error: error.message || error });
|
||||||
|
res.status(error.statusCode || 500).send({
|
||||||
|
message: error.message || 'Failed to fetch the remote playlist URL.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const dns = require('dns').promises;
|
||||||
const formidable = require('formidable');
|
const formidable = require('formidable');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const net = require('net');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { format } = require("util");
|
const { format } = require('util');
|
||||||
|
|
||||||
|
const MAX_REMOTE_PLAYLIST_BYTES = 2 * 1024 * 1024;
|
||||||
|
|
||||||
const ensureDirectoryExistence = (filePath) => {
|
const ensureDirectoryExistence = (filePath) => {
|
||||||
const dirname = path.dirname(filePath);
|
const dirname = path.dirname(filePath);
|
||||||
@ -13,7 +18,39 @@ const ensureDirectoryExistence = (filePath) => {
|
|||||||
|
|
||||||
ensureDirectoryExistence(dirname);
|
ensureDirectoryExistence(dirname);
|
||||||
fs.mkdirSync(dirname);
|
fs.mkdirSync(dirname);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const getUploadedFile = (files) => {
|
||||||
|
if (!files || !files.file) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.isArray(files.file) ? files.file[0] : files.file;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUploadedFilePath = (file) => {
|
||||||
|
if (!file) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.filepath || file.path || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUploadedFileType = (file) => {
|
||||||
|
if (!file) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.mimetype || file.type || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMimeTypeAllowed = (mimeType, validations) => {
|
||||||
|
if (!validations || !validations.allowedMimePrefixes || validations.allowedMimePrefixes.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validations.allowedMimePrefixes.some((prefix) => mimeType.startsWith(prefix));
|
||||||
|
};
|
||||||
|
|
||||||
const uploadLocal = (
|
const uploadLocal = (
|
||||||
folder,
|
folder,
|
||||||
@ -21,30 +58,24 @@ const uploadLocal = (
|
|||||||
entity: null,
|
entity: null,
|
||||||
maxFileSize: null,
|
maxFileSize: null,
|
||||||
folderIncludesAuthenticationUid: false,
|
folderIncludesAuthenticationUid: false,
|
||||||
|
allowAnonymous: false,
|
||||||
|
allowedMimePrefixes: null,
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
return (req, res) => {
|
return (req, res) => {
|
||||||
if (!req.currentUser) {
|
if (!req.currentUser && !validations.allowAnonymous) {
|
||||||
res.sendStatus(403);
|
res.sendStatus(403);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (validations.entity) {
|
||||||
validations.entity
|
|
||||||
) {
|
|
||||||
res.sendStatus(403);
|
res.sendStatus(403);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validations.folderIncludesAuthenticationUid) {
|
if (validations.folderIncludesAuthenticationUid) {
|
||||||
folder = folder.replace(
|
folder = folder.replace(':userId', req.currentUser.authenticationUid);
|
||||||
':userId',
|
if (!req.currentUser.authenticationUid || !folder.includes(req.currentUser.authenticationUid)) {
|
||||||
req.currentUser.authenticationUid,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
!req.currentUser.authenticationUid ||
|
|
||||||
!folder.includes(req.currentUser.authenticationUid)
|
|
||||||
) {
|
|
||||||
res.sendStatus(403);
|
res.sendStatus(403);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -58,140 +89,311 @@ const uploadLocal = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
form.parse(req, function (err, fields, files) {
|
form.parse(req, function (err, fields, files) {
|
||||||
const filename = String(fields.filename);
|
if (err) {
|
||||||
const fileTempUrl = files.file.path;
|
console.error('File upload parse failed:', err);
|
||||||
|
res.status(500).send(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!filename) {
|
const filename = String(fields.filename);
|
||||||
|
const uploadedFile = getUploadedFile(files);
|
||||||
|
const fileTempUrl = getUploadedFilePath(uploadedFile);
|
||||||
|
const mimeType = getUploadedFileType(uploadedFile);
|
||||||
|
|
||||||
|
if (!fileTempUrl || !uploadedFile) {
|
||||||
|
res.status(400).send({ message: 'Please upload a file.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMimeTypeAllowed(mimeType, validations)) {
|
||||||
|
fs.unlinkSync(fileTempUrl);
|
||||||
|
res.status(400).send({ message: 'Invalid file type.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filename || filename === 'undefined') {
|
||||||
fs.unlinkSync(fileTempUrl);
|
fs.unlinkSync(fileTempUrl);
|
||||||
res.sendStatus(500);
|
res.sendStatus(500);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const privateUrl = path.join(
|
const privateUrl = path.join(folder, filename);
|
||||||
form.uploadDir,
|
const destinationPath = path.join(form.uploadDir, privateUrl);
|
||||||
folder,
|
ensureDirectoryExistence(destinationPath);
|
||||||
filename,
|
fs.renameSync(fileTempUrl, destinationPath);
|
||||||
);
|
res.status(200).send({ privateUrl });
|
||||||
ensureDirectoryExistence(privateUrl);
|
|
||||||
fs.renameSync(fileTempUrl, privateUrl);
|
|
||||||
res.sendStatus(200);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
form.on('error', function (err) {
|
form.on('error', function (err) {
|
||||||
|
console.error('File upload failed:', err);
|
||||||
res.status(500).send(err);
|
res.status(500).send(err);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const downloadLocal = async (req, res) => {
|
const downloadLocal = async (req, res) => {
|
||||||
const privateUrl = req.query.privateUrl;
|
const privateUrl = req.query.privateUrl;
|
||||||
if (!privateUrl) {
|
if (!privateUrl) {
|
||||||
return res.sendStatus(404);
|
return res.sendStatus(404);
|
||||||
|
}
|
||||||
|
return res.download(path.join(config.uploadDir, privateUrl));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPrivateIpv4 = (ip) => {
|
||||||
|
const [firstOctet, secondOctet] = ip.split('.').map((segment) => Number(segment));
|
||||||
|
|
||||||
|
if (firstOctet === 10 || firstOctet === 127 || firstOctet === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstOctet === 169 && secondOctet === 254) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstOctet === 172 && secondOctet >= 16 && secondOctet <= 31) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstOctet === 192 && secondOctet === 168) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPrivateIpv6 = (ip) => {
|
||||||
|
const loweredIp = ip.toLowerCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
loweredIp === '::1' ||
|
||||||
|
loweredIp === '::' ||
|
||||||
|
loweredIp.startsWith('fc') ||
|
||||||
|
loweredIp.startsWith('fd') ||
|
||||||
|
loweredIp.startsWith('fe80')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPrivateIp = (ip) => {
|
||||||
|
const family = net.isIP(ip);
|
||||||
|
|
||||||
|
if (family === 4) {
|
||||||
|
return isPrivateIpv4(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (family === 6) {
|
||||||
|
return isPrivateIpv6(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertRemoteUrlIsPublic = async (remoteUrl) => {
|
||||||
|
let parsedUrl;
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsedUrl = new URL(remoteUrl);
|
||||||
|
} catch (error) {
|
||||||
|
const invalidUrlError = new Error('Use a valid http:// or https:// playlist URL.');
|
||||||
|
invalidUrlError.statusCode = 400;
|
||||||
|
throw invalidUrlError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
||||||
|
const invalidProtocolError = new Error('Only http:// and https:// playlist URLs are supported.');
|
||||||
|
invalidProtocolError.statusCode = 400;
|
||||||
|
throw invalidProtocolError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostname = parsedUrl.hostname.toLowerCase();
|
||||||
|
if (hostname === 'localhost' || hostname.endsWith('.local')) {
|
||||||
|
const localhostError = new Error('Local network playlist URLs are not allowed.');
|
||||||
|
localhostError.statusCode = 400;
|
||||||
|
throw localhostError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (net.isIP(hostname) && isPrivateIp(hostname)) {
|
||||||
|
const privateIpError = new Error('Private network playlist URLs are not allowed.');
|
||||||
|
privateIpError.statusCode = 400;
|
||||||
|
throw privateIpError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedAddresses = await dns.lookup(parsedUrl.hostname, { all: true });
|
||||||
|
|
||||||
|
if (!resolvedAddresses.length) {
|
||||||
|
const dnsError = new Error('Could not resolve the playlist host.');
|
||||||
|
dnsError.statusCode = 400;
|
||||||
|
throw dnsError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolvedAddresses.some((entry) => isPrivateIp(entry.address))) {
|
||||||
|
const resolvedPrivateIpError = new Error('Playlist host resolves to a private network address.');
|
||||||
|
resolvedPrivateIpError.statusCode = 400;
|
||||||
|
throw resolvedPrivateIpError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedUrl.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchPublicTextFile = async (remoteUrl) => {
|
||||||
|
const safeUrl = await assertRemoteUrlIsPublic(remoteUrl);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(safeUrl, {
|
||||||
|
responseType: 'text',
|
||||||
|
timeout: 15000,
|
||||||
|
maxContentLength: MAX_REMOTE_PLAYLIST_BYTES,
|
||||||
|
maxBodyLength: MAX_REMOTE_PLAYLIST_BYTES,
|
||||||
|
transformResponse: [(data) => data],
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json, application/x-mpegURL, application/vnd.apple.mpegurl, audio/mpegurl, text/plain;q=0.9,*/*;q=0.5',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof response.data !== 'string') {
|
||||||
|
const responseTypeError = new Error('The remote playlist must return text-based JSON, M3U, or M3U8 content.');
|
||||||
|
responseTypeError.statusCode = 400;
|
||||||
|
throw responseTypeError;
|
||||||
}
|
}
|
||||||
res.download(path.join(config.uploadDir, privateUrl));
|
|
||||||
}
|
return {
|
||||||
|
url: safeUrl,
|
||||||
|
body: response.data,
|
||||||
|
contentType: response.headers['content-type'] || '',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response) {
|
||||||
|
console.error('Remote playlist fetch failed:', {
|
||||||
|
url: safeUrl,
|
||||||
|
status: error.response.status,
|
||||||
|
data: error.response.data,
|
||||||
|
});
|
||||||
|
const remoteResponseError = new Error(`Remote server responded with status ${error.response.status}.`);
|
||||||
|
remoteResponseError.statusCode = error.response.status;
|
||||||
|
throw remoteResponseError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code === 'ERR_BAD_RESPONSE' || error.code === 'ERR_BAD_REQUEST') {
|
||||||
|
console.error('Remote playlist response handling failed:', { url: safeUrl, message: error.message });
|
||||||
|
const remoteBodyError = new Error('The remote playlist response could not be processed.');
|
||||||
|
remoteBodyError.statusCode = 400;
|
||||||
|
throw remoteBodyError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code === 'ECONNABORTED') {
|
||||||
|
console.error('Remote playlist request timed out:', { url: safeUrl, message: error.message });
|
||||||
|
const timeoutError = new Error('Timed out while fetching the remote playlist.');
|
||||||
|
timeoutError.statusCode = 504;
|
||||||
|
throw timeoutError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.message && error.message.includes('maxContentLength')) {
|
||||||
|
console.error('Remote playlist exceeded size limit:', { url: safeUrl, message: error.message });
|
||||||
|
const sizeError = new Error('Remote playlist is larger than the 2 MB import limit.');
|
||||||
|
sizeError.statusCode = 413;
|
||||||
|
throw sizeError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.statusCode) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('Remote playlist fetch failed:', { url: safeUrl, error });
|
||||||
|
const unknownError = new Error('Failed to fetch the remote playlist URL.');
|
||||||
|
unknownError.statusCode = 500;
|
||||||
|
throw unknownError;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const initGCloud = () => {
|
const initGCloud = () => {
|
||||||
const processFile = require("../middlewares/upload");
|
const processFile = require('../middlewares/upload');
|
||||||
const { Storage } = require("@google-cloud/storage");
|
const { Storage } = require('@google-cloud/storage');
|
||||||
|
|
||||||
const crypto = require('crypto')
|
const hash = config.gcloud.hash;
|
||||||
const hash = config.gcloud.hash
|
const privateKey = process.env.GC_PRIVATE_KEY.replace(/\\\n/g, '\n');
|
||||||
|
|
||||||
const privateKey = process.env.GC_PRIVATE_KEY.replace(/\\\n/g, "\n");
|
|
||||||
|
|
||||||
const storage = new Storage({
|
const storage = new Storage({
|
||||||
projectId: process.env.GC_PROJECT_ID,
|
projectId: process.env.GC_PROJECT_ID,
|
||||||
credentials: {
|
credentials: {
|
||||||
client_email: process.env.GC_CLIENT_EMAIL,
|
client_email: process.env.GC_CLIENT_EMAIL,
|
||||||
private_key: privateKey
|
private_key: privateKey,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const bucket = storage.bucket(config.gcloud.bucket);
|
const bucket = storage.bucket(config.gcloud.bucket);
|
||||||
return {hash, bucket, processFile};
|
return { hash, bucket, processFile };
|
||||||
}
|
};
|
||||||
|
|
||||||
const uploadGCloud = async (folder, req, res) => {
|
const uploadGCloud = async (folder, req, res) => {
|
||||||
try {
|
try {
|
||||||
const {hash, bucket, processFile} = initGCloud();
|
const { hash, bucket, processFile } = initGCloud();
|
||||||
await processFile(req, res);
|
await processFile(req, res);
|
||||||
let buffer = await req.file.buffer;
|
const buffer = await req.file.buffer;
|
||||||
let filename = await req.body.filename;
|
const filename = await req.body.filename;
|
||||||
|
|
||||||
if (!req.file) {
|
if (!req.file) {
|
||||||
return res.status(400).send({ message: "Please upload a file!" });
|
return res.status(400).send({ message: 'Please upload a file!' });
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = `${hash}/${folder}/${filename}`;
|
const fullPath = `${hash}/${folder}/${filename}`;
|
||||||
let blob = bucket.file(path);
|
const blob = bucket.file(fullPath);
|
||||||
|
|
||||||
console.log(path);
|
|
||||||
|
|
||||||
const blobStream = blob.createWriteStream({
|
const blobStream = blob.createWriteStream({
|
||||||
resumable: false,
|
resumable: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
blobStream.on("error", (err) => {
|
blobStream.on('error', (err) => {
|
||||||
console.log('Upload error');
|
console.error('Upload error:', err.message);
|
||||||
console.log(err.message);
|
|
||||||
res.status(500).send({ message: err.message });
|
res.status(500).send({ message: err.message });
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`https://storage.googleapis.com/${bucket.name}/${blob.name}`);
|
blobStream.on('finish', async () => {
|
||||||
|
const publicUrl = format(`https://storage.googleapis.com/${bucket.name}/${blob.name}`);
|
||||||
blobStream.on("finish", async (data) => {
|
|
||||||
const publicUrl = format(
|
|
||||||
`https://storage.googleapis.com/${bucket.name}/${blob.name}`
|
|
||||||
);
|
|
||||||
|
|
||||||
res.status(200).send({
|
res.status(200).send({
|
||||||
message: "Uploaded the file successfully: " + path,
|
message: `Uploaded the file successfully: ${fullPath}`,
|
||||||
url: publicUrl,
|
url: publicUrl,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
blobStream.end(buffer)
|
blobStream.end(buffer);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.error('Could not upload the file:', err);
|
||||||
|
|
||||||
res.status(500).send({
|
res.status(500).send({
|
||||||
message: `Could not upload the file. ${err}`
|
message: `Could not upload the file. ${err}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const downloadGCloud = async (req, res) => {
|
const downloadGCloud = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const {hash, bucket, processFile} = initGCloud();
|
const { hash, bucket } = initGCloud();
|
||||||
|
|
||||||
const privateUrl = await req.query.privateUrl;
|
const privateUrl = await req.query.privateUrl;
|
||||||
const filePath = `${hash}/${privateUrl}`;
|
const filePath = `${hash}/${privateUrl}`;
|
||||||
const file = bucket.file(filePath)
|
const file = bucket.file(filePath);
|
||||||
const fileExists = await file.exists();
|
const fileExists = await file.exists();
|
||||||
|
|
||||||
if (fileExists[0]) {
|
if (fileExists[0]) {
|
||||||
const stream = file.createReadStream();
|
const stream = file.createReadStream();
|
||||||
stream.pipe(res);
|
stream.pipe(res);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
res.status(404).send({
|
res.status(404).send({
|
||||||
message: "Could not download the file. " + err,
|
message: 'Could not download the requested file.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(404).send({
|
res.status(404).send({
|
||||||
message: "Could not download the file. " + err,
|
message: `Could not download the file. ${err}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const deleteGCloud = async (privateUrl) => {
|
const deleteGCloud = async (privateUrl) => {
|
||||||
try {
|
try {
|
||||||
const {hash, bucket, processFile} = initGCloud();
|
const { hash, bucket } = initGCloud();
|
||||||
const filePath = `${hash}/${privateUrl}`;
|
const filePath = `${hash}/${privateUrl}`;
|
||||||
|
|
||||||
const file = bucket.file(filePath)
|
const file = bucket.file(filePath);
|
||||||
const fileExists = await file.exists();
|
const fileExists = await file.exists();
|
||||||
|
|
||||||
if (fileExists[0]) {
|
if (fileExists[0]) {
|
||||||
@ -200,7 +402,7 @@ const deleteGCloud = async (privateUrl) => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(`Cannot find the file ${privateUrl}`);
|
console.log(`Cannot find the file ${privateUrl}`);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
initGCloud,
|
initGCloud,
|
||||||
@ -208,6 +410,6 @@ module.exports = {
|
|||||||
downloadLocal,
|
downloadLocal,
|
||||||
deleteGCloud,
|
deleteGCloud,
|
||||||
uploadGCloud,
|
uploadGCloud,
|
||||||
downloadGCloud
|
downloadGCloud,
|
||||||
}
|
fetchPublicTextFile,
|
||||||
|
};
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export type HubLink = {
|
|||||||
|
|
||||||
export type MediaPresetType = 'radio' | 'tv';
|
export type MediaPresetType = 'radio' | 'tv';
|
||||||
export type MediaPresetMode = 'audio' | 'video' | 'embed';
|
export type MediaPresetMode = 'audio' | 'video' | 'embed';
|
||||||
|
export type MediaPresetSourceKind = 'manual' | 'imported' | 'uploaded';
|
||||||
|
|
||||||
export type MediaPreset = {
|
export type MediaPreset = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -21,6 +22,15 @@ export type MediaPreset = {
|
|||||||
notes: string;
|
notes: string;
|
||||||
mode: MediaPresetMode;
|
mode: MediaPresetMode;
|
||||||
isSample?: boolean;
|
isSample?: boolean;
|
||||||
|
sourceKind?: MediaPresetSourceKind;
|
||||||
|
privateUrl?: string;
|
||||||
|
originalFilename?: string;
|
||||||
|
durationSeconds?: number;
|
||||||
|
thumbnailUrl?: string;
|
||||||
|
folder?: string;
|
||||||
|
remotePlaylistUrl?: string;
|
||||||
|
remoteRefreshIntervalMinutes?: number;
|
||||||
|
lastRemoteRefreshAt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdminShortcut = {
|
export type AdminShortcut = {
|
||||||
@ -122,6 +132,7 @@ export const starterMediaPresets: MediaPreset[] = [
|
|||||||
notes: 'Replace this with your live radio stream URL when you are ready.',
|
notes: 'Replace this with your live radio stream URL when you are ready.',
|
||||||
mode: 'audio',
|
mode: 'audio',
|
||||||
isSample: true,
|
isSample: true,
|
||||||
|
sourceKind: 'manual',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'sample-tv',
|
id: 'sample-tv',
|
||||||
@ -131,6 +142,7 @@ export const starterMediaPresets: MediaPreset[] = [
|
|||||||
notes: 'This placeholder shows the TV widget layout until you paste a real embed or video stream.',
|
notes: 'This placeholder shows the TV widget layout until you paste a real embed or video stream.',
|
||||||
mode: 'video',
|
mode: 'video',
|
||||||
isSample: true,
|
isSample: true,
|
||||||
|
sourceKind: 'manual',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user