Changeset View
Changeset View
Standalone View
Standalone View
keyserver/src/fetchers/upload-fetchers.js
// @flow | // @flow | ||||
import ip from 'internal-ip'; | import ip from 'internal-ip'; | ||||
import _keyBy from 'lodash/fp/keyBy.js'; | import _keyBy from 'lodash/fp/keyBy.js'; | ||||
import type { Media, Image, EncryptedImage } from 'lib/types/media-types.js'; | import type { Media, Image, EncryptedImage } from 'lib/types/media-types.js'; | ||||
import { messageTypes } from 'lib/types/message-types-enum.js'; | import { messageTypes } from 'lib/types/message-types-enum.js'; | ||||
import type { MediaMessageServerDBContent } from 'lib/types/messages/media.js'; | import type { MediaMessageServerDBContent } from 'lib/types/messages/media.js'; | ||||
import { getUploadIDsFromMediaMessageServerDBContents } from 'lib/types/messages/media.js'; | import { getUploadIDsFromMediaMessageServerDBContents } from 'lib/types/messages/media.js'; | ||||
import { threadPermissions } from 'lib/types/thread-types.js'; | import { threadPermissions } from 'lib/types/thread-types.js'; | ||||
import type { | import type { | ||||
ThreadFetchMediaResult, | ThreadFetchMediaResult, | ||||
ThreadFetchMediaRequest, | ThreadFetchMediaRequest, | ||||
} from 'lib/types/thread-types.js'; | } from 'lib/types/thread-types.js'; | ||||
import { makeBlobServiceURI } from 'lib/utils/blob-service.js'; | |||||
import { isDev } from 'lib/utils/dev-utils.js'; | import { isDev } from 'lib/utils/dev-utils.js'; | ||||
import { ServerError } from 'lib/utils/errors.js'; | import { ServerError } from 'lib/utils/errors.js'; | ||||
import { dbQuery, SQL } from '../database/database.js'; | import { dbQuery, SQL } from '../database/database.js'; | ||||
import type { Viewer } from '../session/viewer.js'; | import type { Viewer } from '../session/viewer.js'; | ||||
import { getAndAssertCommAppURLFacts } from '../utils/urls.js'; | import { getAndAssertCommAppURLFacts } from '../utils/urls.js'; | ||||
type UploadInfo = { | type UploadInfo = { | ||||
content: Buffer, | content: Buffer, | ||||
mime: string, | mime: string, | ||||
}; | }; | ||||
async function fetchUpload( | async function fetchUpload( | ||||
viewer: Viewer, | viewer: Viewer, | ||||
id: string, | id: string, | ||||
secret: string, | secret: string, | ||||
): Promise<UploadInfo> { | ): Promise<UploadInfo> { | ||||
const query = SQL` | const query = SQL` | ||||
SELECT content, mime | SELECT content, mime, extra | ||||
FROM uploads | FROM uploads | ||||
WHERE id = ${id} AND secret = ${secret} | WHERE id = ${id} AND secret = ${secret} | ||||
`; | `; | ||||
const [result] = await dbQuery(query); | const [result] = await dbQuery(query); | ||||
if (result.length === 0) { | if (result.length === 0) { | ||||
throw new ServerError('invalid_parameters'); | throw new ServerError('invalid_parameters'); | ||||
} | } | ||||
const [row] = result; | const [row] = result; | ||||
const { content, mime } = row; | const { content, mime, extra } = row; | ||||
const { blobHolder } = JSON.parse(extra); | |||||
if (blobHolder) { | |||||
throw new ServerError('resource_unavailable'); | |||||
} | |||||
return { content, mime }; | return { content, mime }; | ||||
} | } | ||||
async function fetchUploadChunk( | async function fetchUploadChunk( | ||||
id: string, | id: string, | ||||
secret: string, | secret: string, | ||||
pos: number, | pos: number, | ||||
len: number, | len: number, | ||||
): Promise<UploadInfo> { | ): Promise<UploadInfo> { | ||||
// We use pos + 1 because SQL is 1-indexed whereas js is 0-indexed | // We use pos + 1 because SQL is 1-indexed whereas js is 0-indexed | ||||
const query = SQL` | const query = SQL` | ||||
SELECT SUBSTRING(content, ${pos + 1}, ${len}) AS content, mime | SELECT SUBSTRING(content, ${pos + 1}, ${len}) AS content, mime, extra | ||||
FROM uploads | FROM uploads | ||||
WHERE id = ${id} AND secret = ${secret} | WHERE id = ${id} AND secret = ${secret} | ||||
`; | `; | ||||
const [result] = await dbQuery(query); | const [result] = await dbQuery(query); | ||||
if (result.length === 0) { | if (result.length === 0) { | ||||
throw new ServerError('invalid_parameters'); | throw new ServerError('invalid_parameters'); | ||||
} | } | ||||
const [row] = result; | const [row] = result; | ||||
const { content, mime } = row; | const { content, mime, extra } = row; | ||||
if (extra) { | |||||
const { blobHolder } = JSON.parse(extra); | |||||
if (blobHolder) { | |||||
throw new ServerError('resource_unavailable'); | |||||
} | |||||
} | |||||
return { | return { | ||||
content, | content, | ||||
mime, | mime, | ||||
}; | }; | ||||
} | } | ||||
// Returns total size in bytes. | // Returns total size in bytes. | ||||
async function getUploadSize(id: string, secret: string): Promise<number> { | async function getUploadSize(id: string, secret: string): Promise<number> { | ||||
const query = SQL` | const query = SQL` | ||||
SELECT LENGTH(content) AS length | SELECT LENGTH(content) AS length, extra | ||||
FROM uploads | FROM uploads | ||||
WHERE id = ${id} AND secret = ${secret} | WHERE id = ${id} AND secret = ${secret} | ||||
`; | `; | ||||
const [result] = await dbQuery(query); | const [result] = await dbQuery(query); | ||||
if (result.length === 0) { | if (result.length === 0) { | ||||
throw new ServerError('invalid_parameters'); | throw new ServerError('invalid_parameters'); | ||||
} | } | ||||
const [row] = result; | const [row] = result; | ||||
const { length } = row; | const { length, extra } = row; | ||||
if (extra) { | |||||
const { blobHolder } = JSON.parse(extra); | |||||
if (blobHolder) { | |||||
throw new ServerError('resource_unavailable'); | |||||
} | |||||
} | |||||
return length; | return length; | ||||
} | } | ||||
function getUploadURL(id: string, secret: string): string { | function getUploadURL(id: string, secret: string): string { | ||||
const { baseDomain, basePath } = getAndAssertCommAppURLFacts(); | const { baseDomain, basePath } = getAndAssertCommAppURLFacts(); | ||||
const uploadPath = `${basePath}upload/${id}/${secret}`; | const uploadPath = `${basePath}upload/${id}/${secret}`; | ||||
if (isDev) { | if (isDev) { | ||||
const ipV4 = ip.v4.sync() || 'localhost'; | const ipV4 = ip.v4.sync() || 'localhost'; | ||||
const port = parseInt(process.env.PORT, 10) || 3000; | const port = parseInt(process.env.PORT, 10) || 3000; | ||||
return `http://${ipV4}:${port}${uploadPath}`; | return `http://${ipV4}:${port}${uploadPath}`; | ||||
} | } | ||||
return `${baseDomain}${uploadPath}`; | return `${baseDomain}${uploadPath}`; | ||||
} | } | ||||
function makeUploadURI(holder: ?string, id: string, secret: string): string { | |||||
if (holder) { | |||||
return makeBlobServiceURI(holder); | |||||
} | |||||
return getUploadURL(id, secret); | |||||
} | |||||
function imagesFromRow(row: Object): Image | EncryptedImage { | function imagesFromRow(row: Object): Image | EncryptedImage { | ||||
const uploadExtra = JSON.parse(row.uploadExtra); | const uploadExtra = JSON.parse(row.uploadExtra); | ||||
const { width, height } = uploadExtra; | const { width, height, blobHolder } = uploadExtra; | ||||
const { uploadType: type, uploadSecret: secret } = row; | const { uploadType: type, uploadSecret: secret } = row; | ||||
const id = row.uploadID.toString(); | const id = row.uploadID.toString(); | ||||
const dimensions = { width, height }; | const dimensions = { width, height }; | ||||
const uri = getUploadURL(id, secret); | const uri = makeUploadURI(blobHolder, id, secret); | ||||
const isEncrypted = !!uploadExtra.encryptionKey; | const isEncrypted = !!uploadExtra.encryptionKey; | ||||
if (type !== 'photo') { | if (type !== 'photo') { | ||||
throw new ServerError('invalid_parameters'); | throw new ServerError('invalid_parameters'); | ||||
} | } | ||||
if (!isEncrypted) { | if (!isEncrypted) { | ||||
return { id, type: 'photo', uri, dimensions }; | return { id, type: 'photo', uri, dimensions }; | ||||
} | } | ||||
return { | return { | ||||
▲ Show 20 Lines • Show All 67 Lines • ▼ Show 20 Lines | const query = SQL` | ||||
ORDER BY creation_time DESC | ORDER BY creation_time DESC | ||||
LIMIT ${request.limit} OFFSET ${request.offset} | LIMIT ${request.limit} OFFSET ${request.offset} | ||||
`; | `; | ||||
const [uploads] = await dbQuery(query); | const [uploads] = await dbQuery(query); | ||||
const media = uploads.map(upload => { | const media = uploads.map(upload => { | ||||
const { uploadID, uploadType, uploadSecret, uploadExtra } = upload; | const { uploadID, uploadType, uploadSecret, uploadExtra } = upload; | ||||
const { width, height, encryptionKey } = JSON.parse(uploadExtra); | const { width, height, encryptionKey, blobHolder } = | ||||
JSON.parse(uploadExtra); | |||||
const dimensions = { width, height }; | const dimensions = { width, height }; | ||||
const uri = makeUploadURI(blobHolder, uploadID, uploadSecret); | |||||
if (uploadType === 'photo') { | if (uploadType === 'photo') { | ||||
if (encryptionKey) { | if (encryptionKey) { | ||||
return { | return { | ||||
type: 'encrypted_photo', | type: 'encrypted_photo', | ||||
id: uploadID.toString(), | id: uploadID.toString(), | ||||
holder: getUploadURL(uploadID, uploadSecret), | holder: uri, | ||||
encryptionKey, | encryptionKey, | ||||
dimensions, | dimensions, | ||||
}; | }; | ||||
} | } | ||||
return { | return { | ||||
type: 'photo', | type: 'photo', | ||||
id: uploadID.toString(), | id: uploadID.toString(), | ||||
uri: getUploadURL(uploadID, uploadSecret), | uri, | ||||
dimensions, | dimensions, | ||||
}; | }; | ||||
} | } | ||||
const { thumbnailID, thumbnailUploadSecret, thumbnailUploadExtra } = upload; | const { thumbnailID, thumbnailUploadSecret, thumbnailUploadExtra } = upload; | ||||
const { | |||||
encryptionKey: thumbnailEncryptionKey, | |||||
blobHolder: thumbnailBlobHolder, | |||||
} = JSON.parse(thumbnailUploadExtra); | |||||
const thumbnailURI = makeUploadURI( | |||||
thumbnailBlobHolder, | |||||
thumbnailID, | |||||
thumbnailUploadSecret, | |||||
); | |||||
if (encryptionKey) { | if (encryptionKey) { | ||||
const { encryptionKey: thumbnailEncryptionKey } = | |||||
JSON.parse(thumbnailUploadExtra); | |||||
return { | return { | ||||
type: 'encrypted_video', | type: 'encrypted_video', | ||||
id: uploadID.toString(), | id: uploadID.toString(), | ||||
holder: getUploadURL(uploadID, uploadSecret), | holder: uri, | ||||
encryptionKey, | encryptionKey, | ||||
dimensions, | dimensions, | ||||
thumbnailID, | thumbnailID, | ||||
thumbnailHolder: getUploadURL(thumbnailID, thumbnailUploadSecret), | thumbnailHolder: thumbnailURI, | ||||
thumbnailEncryptionKey, | thumbnailEncryptionKey, | ||||
}; | }; | ||||
} | } | ||||
return { | return { | ||||
type: 'video', | type: 'video', | ||||
id: uploadID.toString(), | id: uploadID.toString(), | ||||
uri: getUploadURL(uploadID, uploadSecret), | uri, | ||||
dimensions, | dimensions, | ||||
thumbnailID, | thumbnailID, | ||||
thumbnailURI: getUploadURL(thumbnailID, thumbnailUploadSecret), | thumbnailURI, | ||||
}; | }; | ||||
}); | }); | ||||
return { media }; | return { media }; | ||||
} | } | ||||
async function fetchUploadsForMessage( | async function fetchUploadsForMessage( | ||||
viewer: Viewer, | viewer: Viewer, | ||||
Show All 30 Lines | |||||
): $ReadOnlyArray<Media> { | ): $ReadOnlyArray<Media> { | ||||
const uploadMap = _keyBy('uploadID')(uploadRows); | const uploadMap = _keyBy('uploadID')(uploadRows); | ||||
const media: Media[] = []; | const media: Media[] = []; | ||||
for (const mediaMessageContent of mediaMessageContents) { | for (const mediaMessageContent of mediaMessageContents) { | ||||
const primaryUploadID = mediaMessageContent.uploadID; | const primaryUploadID = mediaMessageContent.uploadID; | ||||
const primaryUpload = uploadMap[primaryUploadID]; | const primaryUpload = uploadMap[primaryUploadID]; | ||||
const primaryUploadSecret = primaryUpload.uploadSecret; | |||||
const primaryUploadURI = getUploadURL(primaryUploadID, primaryUploadSecret); | |||||
const uploadExtra = JSON.parse(primaryUpload.uploadExtra); | const uploadExtra = JSON.parse(primaryUpload.uploadExtra); | ||||
const { width, height, loop, encryptionKey } = uploadExtra; | const { width, height, loop, blobHolder, encryptionKey } = uploadExtra; | ||||
const dimensions = { width, height }; | const dimensions = { width, height }; | ||||
const primaryUploadURI = makeUploadURI( | |||||
blobHolder, | |||||
primaryUploadID, | |||||
primaryUpload.uploadSecret, | |||||
); | |||||
if (mediaMessageContent.type === 'photo') { | if (mediaMessageContent.type === 'photo') { | ||||
if (encryptionKey) { | if (encryptionKey) { | ||||
media.push({ | media.push({ | ||||
type: 'encrypted_photo', | type: 'encrypted_photo', | ||||
id: primaryUploadID, | id: primaryUploadID, | ||||
holder: primaryUploadURI, | holder: primaryUploadURI, | ||||
encryptionKey, | encryptionKey, | ||||
dimensions, | dimensions, | ||||
}); | }); | ||||
} else { | } else { | ||||
media.push({ | media.push({ | ||||
type: 'photo', | type: 'photo', | ||||
id: primaryUploadID, | id: primaryUploadID, | ||||
uri: primaryUploadURI, | uri: primaryUploadURI, | ||||
dimensions, | dimensions, | ||||
}); | }); | ||||
} | } | ||||
continue; | continue; | ||||
} | } | ||||
const thumbnailUploadID = mediaMessageContent.thumbnailUploadID; | const thumbnailUploadID = mediaMessageContent.thumbnailUploadID; | ||||
const thumbnailUpload = uploadMap[thumbnailUploadID]; | const thumbnailUpload = uploadMap[thumbnailUploadID]; | ||||
const thumbnailUploadSecret = thumbnailUpload.uploadSecret; | const thumbnailUploadExtra = JSON.parse(thumbnailUpload.uploadExtra); | ||||
const thumbnailUploadURI = getUploadURL( | const { blobHolder: thumbnailBlobHolder } = thumbnailUploadExtra; | ||||
const thumbnailUploadURI = makeUploadURI( | |||||
thumbnailBlobHolder, | |||||
thumbnailUploadID, | thumbnailUploadID, | ||||
thumbnailUploadSecret, | thumbnailUpload.uploadSecret, | ||||
); | ); | ||||
const thumbnailUploadExtra = JSON.parse(thumbnailUpload.uploadExtra); | |||||
if (encryptionKey) { | if (encryptionKey) { | ||||
const video = { | const video = { | ||||
type: 'encrypted_video', | type: 'encrypted_video', | ||||
id: primaryUploadID, | id: primaryUploadID, | ||||
holder: primaryUploadURI, | holder: primaryUploadURI, | ||||
encryptionKey, | encryptionKey, | ||||
dimensions, | dimensions, | ||||
Show All 18 Lines | ): $ReadOnlyArray<Media> { | ||||
return media; | return media; | ||||
} | } | ||||
export { | export { | ||||
fetchUpload, | fetchUpload, | ||||
fetchUploadChunk, | fetchUploadChunk, | ||||
getUploadSize, | getUploadSize, | ||||
getUploadURL, | getUploadURL, | ||||
makeUploadURI, | |||||
imagesFromRow, | imagesFromRow, | ||||
fetchImages, | fetchImages, | ||||
fetchMediaForThread, | fetchMediaForThread, | ||||
fetchMediaFromMediaMessageContent, | fetchMediaFromMediaMessageContent, | ||||
constructMediaFromMediaMessageContentsAndUploadRows, | constructMediaFromMediaMessageContentsAndUploadRows, | ||||
}; | }; |