falla desde api
This commit is contained in:
@@ -4,6 +4,7 @@ const multer = require('multer');
|
||||
const path = require('path');
|
||||
const {
|
||||
getTripStatusUploadsDir,
|
||||
getTripStatusFallbackUploadsDir,
|
||||
replicateUploadedFilesToRemote,
|
||||
removeUploadedTripStatusFiles
|
||||
} = require('../services/tripStatusPhotoStorage');
|
||||
@@ -27,9 +28,21 @@ const getTripDirectorySegment = (req) => {
|
||||
|
||||
const getTripStatusUploadsTripDir = (req) =>
|
||||
path.join(getTripStatusUploadsDir(), getTripDirectorySegment(req));
|
||||
const getTripStatusFallbackUploadsTripDir = (req) =>
|
||||
path.join(getTripStatusFallbackUploadsDir(), getTripDirectorySegment(req));
|
||||
|
||||
const ensureTripStatusUploadsDir = (req) => {
|
||||
fs.mkdirSync(getTripStatusUploadsTripDir(req), { recursive: true });
|
||||
const primaryTripDir = getTripStatusUploadsTripDir(req);
|
||||
|
||||
try {
|
||||
fs.mkdirSync(primaryTripDir, { recursive: true });
|
||||
return primaryTripDir;
|
||||
} catch (primaryError) {
|
||||
const fallbackTripDir = getTripStatusFallbackUploadsTripDir(req);
|
||||
|
||||
fs.mkdirSync(fallbackTripDir, { recursive: true });
|
||||
return fallbackTripDir;
|
||||
}
|
||||
};
|
||||
|
||||
const getExtensionFromMimeType = (mimeType) => {
|
||||
@@ -55,8 +68,7 @@ const getExtensionFromMimeType = (mimeType) => {
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
try {
|
||||
ensureTripStatusUploadsDir(req);
|
||||
cb(null, getTripStatusUploadsTripDir(req));
|
||||
cb(null, ensureTripStatusUploadsDir(req));
|
||||
} catch (error) {
|
||||
cb(error);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const {
|
||||
getTripStatusUploadsDir,
|
||||
replicateUploadedFilesToRemote,
|
||||
removeUploadedTripStatusFiles
|
||||
} = require('../services/tripStatusPhotoStorage');
|
||||
const { appendPostLog } = require('../utils/postLog');
|
||||
|
||||
const MAX_TRIP_STATUS_PHOTO_SIZE_BYTES = 15 * 1024 * 1024;
|
||||
const MAX_TRIP_STATUS_FILES = 5;
|
||||
const ALLOWED_MIME_TYPES = new Set([
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
'image/heic',
|
||||
'image/heif'
|
||||
]);
|
||||
|
||||
const getTripDirectorySegment = (req) => {
|
||||
const tripId = Number.parseInt(req.params?.id, 10);
|
||||
return Number.isInteger(tripId) && tripId > 0 ? String(tripId) : 'unknown';
|
||||
};
|
||||
|
||||
const getTripStatusUploadsTripDir = (req) =>
|
||||
path.join(getTripStatusUploadsDir(), getTripDirectorySegment(req));
|
||||
|
||||
const ensureTripStatusUploadsDir = (req) => {
|
||||
fs.mkdirSync(getTripStatusUploadsTripDir(req), { recursive: true });
|
||||
};
|
||||
|
||||
const getExtensionFromMimeType = (mimeType) => {
|
||||
if (mimeType === 'image/png') {
|
||||
return '.png';
|
||||
}
|
||||
|
||||
if (mimeType === 'image/webp') {
|
||||
return '.webp';
|
||||
}
|
||||
|
||||
if (mimeType === 'image/heic') {
|
||||
return '.heic';
|
||||
}
|
||||
|
||||
if (mimeType === 'image/heif') {
|
||||
return '.heif';
|
||||
}
|
||||
|
||||
return '.jpg';
|
||||
};
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
try {
|
||||
ensureTripStatusUploadsDir(req);
|
||||
cb(null, getTripStatusUploadsTripDir(req));
|
||||
} catch (error) {
|
||||
cb(error);
|
||||
}
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const token = crypto.randomBytes(3).toString('hex');
|
||||
const extension = getExtensionFromMimeType(file.mimetype);
|
||||
cb(null, `${token}${extension}`);
|
||||
}
|
||||
});
|
||||
|
||||
const internalUpload = multer({
|
||||
storage,
|
||||
limits: {
|
||||
fileSize: MAX_TRIP_STATUS_PHOTO_SIZE_BYTES,
|
||||
files: MAX_TRIP_STATUS_FILES
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (!ALLOWED_MIME_TYPES.has(file.mimetype)) {
|
||||
return cb(new Error('INVALID_FILE_TYPE'));
|
||||
}
|
||||
|
||||
cb(null, true);
|
||||
}
|
||||
});
|
||||
|
||||
const uploadTripStatusPhotos = (req, res, next) => {
|
||||
internalUpload.fields([
|
||||
{ name: 'fotos', maxCount: MAX_TRIP_STATUS_FILES },
|
||||
{ name: 'fotos[]', maxCount: MAX_TRIP_STATUS_FILES }
|
||||
])(req, res, async (error) => {
|
||||
if (!error) {
|
||||
const uploadedFiles = collectUploadedTripStatusFiles(req);
|
||||
const authorizationHeader = req.get('authorization');
|
||||
|
||||
appendPostLog({
|
||||
request_id: req.requestId || null,
|
||||
method: req.method,
|
||||
path: req.originalUrl || req.url,
|
||||
ip: req.ip || null,
|
||||
content_type: req.get('content-type') || null,
|
||||
has_authorization_header: Boolean(authorizationHeader),
|
||||
authorization: authorizationHeader ? '[REDACTED]' : null,
|
||||
query: req.query || {},
|
||||
body: req.body || {},
|
||||
raw_body: null,
|
||||
files: uploadedFiles.map((file) => ({
|
||||
field_name: file.fieldname || null,
|
||||
filename: file.filename || null,
|
||||
originalname: file.originalname || null,
|
||||
mimetype: file.mimetype || null,
|
||||
size: Number.isFinite(file.size) ? file.size : null
|
||||
}))
|
||||
});
|
||||
await replicateUploadedFilesToRemote({
|
||||
tripId: req.params?.id,
|
||||
files: uploadedFiles
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
if (error instanceof multer.MulterError) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid payload'
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message === 'INVALID_FILE_TYPE') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid payload'
|
||||
});
|
||||
}
|
||||
|
||||
console.error('Error uploading trip status photos:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const collectUploadedTripStatusFiles = (req) => [
|
||||
...(Array.isArray(req.files?.fotos) ? req.files.fotos : []),
|
||||
...(Array.isArray(req.files?.['fotos[]']) ? req.files['fotos[]'] : [])
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
uploadTripStatusPhotos,
|
||||
collectUploadedTripStatusFiles,
|
||||
removeUploadedTripStatusFiles,
|
||||
MAX_TRIP_STATUS_FILES
|
||||
};
|
||||
@@ -6,10 +6,47 @@ const DEFAULT_SFTP_PORT = 22;
|
||||
|
||||
let sftpClientFactoryOverride = null;
|
||||
|
||||
const getTripStatusUploadsDir = () =>
|
||||
process.env.TRIP_STATUS_UPLOAD_DIR ||
|
||||
const PRIMARY_TRIP_STATUS_UPLOAD_DIR =
|
||||
'/var/www/vhosts/gestion.abianservice.com/httpdocs/produccion/app/fotos_estado_react_native/trips/status';
|
||||
const FALLBACK_TRIP_STATUS_UPLOAD_DIR =
|
||||
path.resolve(__dirname, '..', '..', 'uploads', 'trips', 'status');
|
||||
|
||||
const resolveUploadDirCandidate = (uploadDir) =>
|
||||
path.isAbsolute(uploadDir)
|
||||
? uploadDir
|
||||
: path.resolve(__dirname, '..', '..', uploadDir);
|
||||
|
||||
const getExistingAncestorScore = (targetPath) => {
|
||||
let currentPath = targetPath;
|
||||
let score = 0;
|
||||
|
||||
while (currentPath && currentPath !== path.dirname(currentPath)) {
|
||||
if (fs.existsSync(currentPath)) {
|
||||
return score;
|
||||
}
|
||||
|
||||
currentPath = path.dirname(currentPath);
|
||||
score += 1;
|
||||
}
|
||||
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
};
|
||||
|
||||
const selectUploadDirCandidate = (uploadDirs) =>
|
||||
uploadDirs
|
||||
.map((uploadDir, index) => ({
|
||||
index,
|
||||
path: resolveUploadDirCandidate(uploadDir),
|
||||
score: getExistingAncestorScore(resolveUploadDirCandidate(uploadDir))
|
||||
}))
|
||||
.sort((left, right) => left.score - right.score || left.index - right.index)[0]?.path;
|
||||
|
||||
const getTripStatusUploadsDir = () => {
|
||||
return PRIMARY_TRIP_STATUS_UPLOAD_DIR;
|
||||
};
|
||||
|
||||
const getTripStatusFallbackUploadsDir = () => FALLBACK_TRIP_STATUS_UPLOAD_DIR;
|
||||
|
||||
const getTripStatusPhotoStorageMode = () =>
|
||||
String(process.env.TRIP_STATUS_PHOTO_STORAGE_MODE || 'local')
|
||||
.trim()
|
||||
@@ -352,6 +389,7 @@ const __resetSftpClientFactoryForTests = () => {
|
||||
|
||||
module.exports = {
|
||||
getTripStatusUploadsDir,
|
||||
getTripStatusFallbackUploadsDir,
|
||||
replicateUploadedFilesToRemote,
|
||||
removeUploadedTripStatusFiles,
|
||||
removeStatusPhotosByName,
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const REMOTE_STORAGE_MODES = new Set(['dual', 'sftp']);
|
||||
const DEFAULT_SFTP_PORT = 22;
|
||||
|
||||
let sftpClientFactoryOverride = null;
|
||||
|
||||
const getTripStatusUploadsDir = () =>
|
||||
process.env.TRIP_STATUS_UPLOAD_DIR ||
|
||||
path.resolve(__dirname, '..', '..', 'uploads', 'trips', 'status');
|
||||
|
||||
const getTripStatusPhotoStorageMode = () =>
|
||||
String(process.env.TRIP_STATUS_PHOTO_STORAGE_MODE || 'local')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
const shouldUseRemoteStorage = () =>
|
||||
REMOTE_STORAGE_MODES.has(getTripStatusPhotoStorageMode());
|
||||
|
||||
const getTripDirectorySegment = (tripId) => {
|
||||
const parsedTripId = Number.parseInt(tripId, 10);
|
||||
return Number.isInteger(parsedTripId) && parsedTripId > 0 ? String(parsedTripId) : 'unknown';
|
||||
};
|
||||
|
||||
const normalizeRemoteBaseDir = (rawValue) =>
|
||||
String(rawValue || '')
|
||||
.trim()
|
||||
.replace(/\/+$/g, '');
|
||||
|
||||
const getSftpConfig = () => {
|
||||
const host = String(process.env.TRIP_STATUS_SFTP_HOST || '').trim();
|
||||
const username = String(process.env.TRIP_STATUS_SFTP_USERNAME || '').trim();
|
||||
const password = String(process.env.TRIP_STATUS_SFTP_PASSWORD || '').trim();
|
||||
const remoteBaseDir = normalizeRemoteBaseDir(process.env.TRIP_STATUS_SFTP_REMOTE_BASE_DIR);
|
||||
const parsedPort = Number.parseInt(process.env.TRIP_STATUS_SFTP_PORT, 10);
|
||||
const port = Number.isInteger(parsedPort) && parsedPort > 0 ? parsedPort : DEFAULT_SFTP_PORT;
|
||||
const missing = [];
|
||||
|
||||
if (!host) {
|
||||
missing.push('TRIP_STATUS_SFTP_HOST');
|
||||
}
|
||||
if (!username) {
|
||||
missing.push('TRIP_STATUS_SFTP_USERNAME');
|
||||
}
|
||||
if (!password) {
|
||||
missing.push('TRIP_STATUS_SFTP_PASSWORD');
|
||||
}
|
||||
if (!remoteBaseDir) {
|
||||
missing.push('TRIP_STATUS_SFTP_REMOTE_BASE_DIR');
|
||||
}
|
||||
|
||||
return {
|
||||
host,
|
||||
username,
|
||||
password,
|
||||
port,
|
||||
remoteBaseDir,
|
||||
isValid: missing.length === 0,
|
||||
missing
|
||||
};
|
||||
};
|
||||
|
||||
const getSftpClientFactory = () => {
|
||||
if (typeof sftpClientFactoryOverride === 'function') {
|
||||
return sftpClientFactoryOverride;
|
||||
}
|
||||
|
||||
try {
|
||||
const SftpClient = require('ssh2-sftp-client');
|
||||
return () => new SftpClient();
|
||||
} catch (error) {
|
||||
console.error('SFTP client dependency is unavailable for trip status photos:', {
|
||||
message: error.message
|
||||
});
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const isRemoteFileNotFoundError = (error) => {
|
||||
const normalizedMessage = String(error?.message || '').toLowerCase();
|
||||
return (
|
||||
error?.code === 'ENOENT' ||
|
||||
error?.code === 2 ||
|
||||
normalizedMessage.includes('no such file') ||
|
||||
normalizedMessage.includes('not exist')
|
||||
);
|
||||
};
|
||||
|
||||
const buildRemoteTripDir = (remoteBaseDir, tripId) =>
|
||||
path.posix.join(remoteBaseDir, getTripDirectorySegment(tripId));
|
||||
|
||||
const buildRemoteFilePath = (remoteBaseDir, tripId, fileName) =>
|
||||
path.posix.join(buildRemoteTripDir(remoteBaseDir, tripId), fileName);
|
||||
|
||||
const ensureRemoteTripDirectory = async (client, { remoteBaseDir, tripDirectorySegment }) => {
|
||||
const remoteTripDir = buildRemoteTripDir(remoteBaseDir, tripDirectorySegment);
|
||||
|
||||
const baseExists = await client.exists(remoteBaseDir);
|
||||
if (!baseExists) {
|
||||
await client.mkdir(remoteBaseDir, true);
|
||||
}
|
||||
|
||||
const tripDirExists = await client.exists(remoteTripDir);
|
||||
if (!tripDirExists) {
|
||||
await client.mkdir(remoteTripDir, false);
|
||||
}
|
||||
|
||||
return remoteTripDir;
|
||||
};
|
||||
|
||||
const withSftpClient = async (handler, { logContext }) => {
|
||||
if (!shouldUseRemoteStorage()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sftpConfig = getSftpConfig();
|
||||
if (!sftpConfig.isValid) {
|
||||
console.error('Trip status photo remote storage skipped due to missing SFTP config:', {
|
||||
context: logContext,
|
||||
missing: sftpConfig.missing
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const createClient = getSftpClientFactory();
|
||||
if (!createClient) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const client = createClient();
|
||||
try {
|
||||
await client.connect({
|
||||
host: sftpConfig.host,
|
||||
port: sftpConfig.port,
|
||||
username: sftpConfig.username,
|
||||
password: sftpConfig.password
|
||||
});
|
||||
|
||||
await handler(client, sftpConfig);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Trip status photo remote storage operation failed:', {
|
||||
context: logContext,
|
||||
message: error.message
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
try {
|
||||
await client.end();
|
||||
} catch (closeError) {
|
||||
console.error('Failed to close SFTP connection for trip status photos:', {
|
||||
context: logContext,
|
||||
message: closeError.message
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const replicateUploadedFilesToRemote = async ({ tripId, files }) => {
|
||||
const normalizedFiles = (files || []).filter((file) => file?.path && file?.filename);
|
||||
if (normalizedFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tripDirectorySegment = getTripDirectorySegment(tripId);
|
||||
for (const file of normalizedFiles) {
|
||||
file.tripStatusTripId = tripDirectorySegment;
|
||||
}
|
||||
|
||||
await withSftpClient(
|
||||
async (client, sftpConfig) => {
|
||||
await ensureRemoteTripDirectory(client, {
|
||||
remoteBaseDir: sftpConfig.remoteBaseDir,
|
||||
tripDirectorySegment
|
||||
});
|
||||
|
||||
for (const file of normalizedFiles) {
|
||||
const remoteFilePath = buildRemoteFilePath(
|
||||
sftpConfig.remoteBaseDir,
|
||||
tripDirectorySegment,
|
||||
file.filename
|
||||
);
|
||||
|
||||
try {
|
||||
await client.put(file.path, remoteFilePath);
|
||||
file.tripStatusRemoteUploaded = true;
|
||||
file.tripStatusRemotePath = remoteFilePath;
|
||||
} catch (error) {
|
||||
file.tripStatusRemoteUploaded = false;
|
||||
file.tripStatusRemotePath = null;
|
||||
console.error('Failed to replicate trip status photo to SFTP. Local fallback kept:', {
|
||||
tripId: tripDirectorySegment,
|
||||
fileName: file.filename,
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
logContext: 'replicate_upload'
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const removeRemoteFiles = async (remoteFilePaths, { logContext }) => {
|
||||
const uniqueRemotePaths = Array.from(new Set((remoteFilePaths || []).filter(Boolean)));
|
||||
if (uniqueRemotePaths.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await withSftpClient(
|
||||
async (client) => {
|
||||
for (const remoteFilePath of uniqueRemotePaths) {
|
||||
try {
|
||||
await client.delete(remoteFilePath);
|
||||
} catch (error) {
|
||||
if (isRemoteFileNotFoundError(error)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.error('Failed to remove remote trip status photo:', {
|
||||
context: logContext,
|
||||
remoteFilePath,
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{ logContext }
|
||||
);
|
||||
};
|
||||
|
||||
const removeLocalFileIfExists = async (filePath, { logContext, fileName, tripId }) => {
|
||||
try {
|
||||
await fs.promises.unlink(filePath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.error('Failed to remove local trip status photo:', {
|
||||
context: logContext,
|
||||
tripId,
|
||||
file: fileName,
|
||||
path: filePath,
|
||||
message: error.message
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupUploadedTripStatusFiles = async (files) => {
|
||||
const normalizedFiles = (files || []).filter((file) => file?.path || file?.filename);
|
||||
if (normalizedFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sftpConfig = getSftpConfig();
|
||||
const remoteFilePaths = [];
|
||||
for (const file of normalizedFiles) {
|
||||
if (file?.tripStatusRemotePath) {
|
||||
remoteFilePaths.push(file.tripStatusRemotePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
const tripId = file?.tripStatusTripId;
|
||||
if (shouldUseRemoteStorage() && sftpConfig.isValid && tripId && file?.filename) {
|
||||
remoteFilePaths.push(
|
||||
buildRemoteFilePath(sftpConfig.remoteBaseDir, tripId, file.filename)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await removeRemoteFiles(remoteFilePaths, { logContext: 'cleanup_uploaded_files' });
|
||||
|
||||
for (const file of normalizedFiles) {
|
||||
if (!file?.path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await removeLocalFileIfExists(file.path, {
|
||||
logContext: 'cleanup_uploaded_files',
|
||||
fileName: file.filename || null,
|
||||
tripId: file?.tripStatusTripId || null
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const parsePhotoNames = (rawPhotoReferences) =>
|
||||
String(rawPhotoReferences || '')
|
||||
.split(';')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
.filter((item) => !item.includes('/') && !item.includes('\\'));
|
||||
|
||||
const removeStatusPhotosByName = async ({ tripId, rawPhotoReferences }) => {
|
||||
const photoNames = parsePhotoNames(rawPhotoReferences);
|
||||
if (photoNames.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uploadsDir = getTripStatusUploadsDir();
|
||||
const tripUploadsDir = path.join(uploadsDir, getTripDirectorySegment(tripId));
|
||||
|
||||
const sftpConfig = getSftpConfig();
|
||||
const remoteFilePaths =
|
||||
shouldUseRemoteStorage() && sftpConfig.isValid
|
||||
? photoNames.map((photoName) =>
|
||||
buildRemoteFilePath(sftpConfig.remoteBaseDir, tripId, photoName)
|
||||
)
|
||||
: [];
|
||||
|
||||
await removeRemoteFiles(remoteFilePaths, { logContext: 'remove_status_photos_by_name' });
|
||||
|
||||
for (const photoName of photoNames) {
|
||||
const candidatePaths = [
|
||||
path.join(tripUploadsDir, photoName),
|
||||
path.join(uploadsDir, photoName)
|
||||
];
|
||||
|
||||
for (const candidatePath of candidatePaths) {
|
||||
const removed = await removeLocalFileIfExists(candidatePath, {
|
||||
logContext: 'remove_status_photos_by_name',
|
||||
fileName: photoName,
|
||||
tripId
|
||||
});
|
||||
|
||||
if (removed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeUploadedTripStatusFiles = (files) => {
|
||||
cleanupUploadedTripStatusFiles(files).catch((error) => {
|
||||
console.error('Failed to cleanup uploaded trip status files:', {
|
||||
message: error.message
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const __setSftpClientFactoryForTests = (factory) => {
|
||||
sftpClientFactoryOverride = typeof factory === 'function' ? factory : null;
|
||||
};
|
||||
|
||||
const __resetSftpClientFactoryForTests = () => {
|
||||
sftpClientFactoryOverride = null;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getTripStatusUploadsDir,
|
||||
replicateUploadedFilesToRemote,
|
||||
removeUploadedTripStatusFiles,
|
||||
removeStatusPhotosByName,
|
||||
__setSftpClientFactoryForTests,
|
||||
__resetSftpClientFactoryForTests
|
||||
};
|
||||
Reference in New Issue
Block a user