Added RNFS and refactor image upload to use it

This commit is contained in:
John Lyon-Smith
2018-05-14 10:38:38 -07:00
parent 6fae5ef5d6
commit eda43b0869
16 changed files with 174 additions and 109 deletions

View File

@@ -166,6 +166,7 @@ dependencies {
compile 'com.amazonaws:aws-android-sdk-cognitoidentityprovider:2.2.+' compile 'com.amazonaws:aws-android-sdk-cognitoidentityprovider:2.2.+'
compile project(':react-native-maps') compile project(':react-native-maps')
compile project(':react-native-image-picker') compile project(':react-native-image-picker')
compile project(':react-native-fs')
} }
// Run this once to be able to run the application with BUCK // Run this once to be able to run the application with BUCK

View File

@@ -17,6 +17,8 @@ import com.airbnb.android.react.maps.MapsPackage;
import com.imagepicker.ImagePickerPackage; import com.imagepicker.ImagePickerPackage;
import com.rnfs.RNFSPackage;
public class MainApplication extends Application implements ReactApplication { public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@@ -31,7 +33,8 @@ public class MainApplication extends Application implements ReactApplication {
new MainReactPackage(), new MainReactPackage(),
new ReactViroPackage(ReactViroPackage.ViroPlatform.GVR), new ReactViroPackage(ReactViroPackage.ViroPlatform.GVR),
new MapsPackage(), new MapsPackage(),
new ImagePickerPackage() new ImagePickerPackage(),
new RNFSPackage()
); );
} }

View File

@@ -13,3 +13,6 @@ project(':react-native-maps').projectDir = new File(rootProject.projectDir, '../
include ':react-native-image-picker' include ':react-native-image-picker'
project(':react-native-image-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-picker/android') project(':react-native-image-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-picker/android')
include ':react-native-fs'
project(':react-native-fs').projectDir = new File(settingsDir, '../node_modules/react-native-fs/android')

View File

@@ -201,6 +201,7 @@
"${BUILT_PRODUCTS_DIR}/Folly/folly.framework", "${BUILT_PRODUCTS_DIR}/Folly/folly.framework",
"${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework",
"${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework",
"${BUILT_PRODUCTS_DIR}/RNFS/RNFS.framework",
"${BUILT_PRODUCTS_DIR}/React/React.framework", "${BUILT_PRODUCTS_DIR}/React/React.framework",
"${PODS_ROOT}/../../node_modules/react-viro/ios/dist/ViroRenderer/ViroKit.framework", "${PODS_ROOT}/../../node_modules/react-viro/ios/dist/ViroRenderer/ViroKit.framework",
"${BUILT_PRODUCTS_DIR}/glog/glog.framework", "${BUILT_PRODUCTS_DIR}/glog/glog.framework",
@@ -216,6 +217,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/folly.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/folly.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNFS.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ViroKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ViroKit.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework",

View File

@@ -35,6 +35,8 @@ target 'DeightonAR' do
pod 'ViroKit', :path => '../node_modules/react-viro/ios/dist/ViroRenderer/' pod 'ViroKit', :path => '../node_modules/react-viro/ios/dist/ViroRenderer/'
pod 'react-native-image-picker', :path => '../node_modules/react-native-image-picker' pod 'react-native-image-picker', :path => '../node_modules/react-native-image-picker'
pod 'RNFS', :path => '../node_modules/react-native-fs'
end end
# See https://gist.github.com/Jpunt/3fe75effd54a702034b75ff697e47578 # See https://gist.github.com/Jpunt/3fe75effd54a702034b75ff697e47578

View File

@@ -60,6 +60,8 @@ PODS:
- React/Core - React/Core
- React/fishhook - React/fishhook
- React/RCTBlob - React/RCTBlob
- RNFS (2.9.12):
- React
- ViroKit (1.0): - ViroKit (1.0):
- AWSDynamoDB (~> 2.6.7) - AWSDynamoDB (~> 2.6.7)
- GVRAudioSDK (= 1.120.0) - GVRAudioSDK (= 1.120.0)
@@ -82,6 +84,7 @@ DEPENDENCIES:
- React/RCTNetwork (from `../node_modules/react-native`) - React/RCTNetwork (from `../node_modules/react-native`)
- React/RCTText (from `../node_modules/react-native`) - React/RCTText (from `../node_modules/react-native`)
- React/RCTWebSocket (from `../node_modules/react-native`) - React/RCTWebSocket (from `../node_modules/react-native`)
- RNFS (from `../node_modules/react-native-fs`)
- ViroKit (from `../node_modules/react-viro/ios/dist/ViroRenderer/`) - ViroKit (from `../node_modules/react-viro/ios/dist/ViroRenderer/`)
- ViroReact (from `../node_modules/react-viro/ios/`) - ViroReact (from `../node_modules/react-viro/ios/`)
- yoga (from `../node_modules/react-native/ReactCommon/yoga`) - yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@@ -109,6 +112,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-image-picker" :path: "../node_modules/react-native-image-picker"
react-native-maps: react-native-maps:
:path: "../node_modules/react-native-maps" :path: "../node_modules/react-native-maps"
RNFS:
:path: "../node_modules/react-native-fs"
ViroKit: ViroKit:
:path: "../node_modules/react-viro/ios/dist/ViroRenderer/" :path: "../node_modules/react-viro/ios/dist/ViroRenderer/"
ViroReact: ViroReact:
@@ -130,10 +135,11 @@ SPEC CHECKSUMS:
React: aa2040dbb6f317b95314968021bd2888816e03d5 React: aa2040dbb6f317b95314968021bd2888816e03d5
react-native-image-picker: 42cfe2c8435d893414f8714a81e480313cb1412b react-native-image-picker: 42cfe2c8435d893414f8714a81e480313cb1412b
react-native-maps: 066c2afcc89e18726377bcc685315f989ca22449 react-native-maps: 066c2afcc89e18726377bcc685315f989ca22449
RNFS: bbb1a64eb245763daf34aea86f97c97c4e85f74c
ViroKit: 9631f301ef6a3f56116b23d6aac5d5c2307aa368 ViroKit: 9631f301ef6a3f56116b23d6aac5d5c2307aa368
ViroReact: 5520f26ac4654e361786c82da3b29ce0402c3c00 ViroReact: 5520f26ac4654e361786c82da3b29ce0402c3c00
yoga: a23273df0088bf7f2bb7e5d7b00044ea57a2a54a yoga: a23273df0088bf7f2bb7e5d7b00044ea57a2a54a
PODFILE CHECKSUM: bf7fd2f2a19c210b54a09cfb216d5f930cf6601c PODFILE CHECKSUM: 5c148f4a189f391c884f82181ca6fc7bf1d45d9c
COCOAPODS: 1.5.0 COCOAPODS: 1.5.0

View File

@@ -2051,6 +2051,11 @@
} }
} }
}, },
"base-64": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
"integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs="
},
"base64-arraybuffer": { "base64-arraybuffer": {
"version": "0.1.5", "version": "0.1.5",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
@@ -5214,6 +5219,15 @@
"prop-types": "^15.5.10" "prop-types": "^15.5.10"
} }
}, },
"react-native-fs": {
"version": "2.9.12",
"resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.9.12.tgz",
"integrity": "sha512-kppfQwMEmEerP9KImdzRi49ko0pmZtzHzIhpDSkjQVrpTPG7AEzYxdAqapaDveHM8CcN6tzqmiljrueqlBr1VA==",
"requires": {
"base-64": "^0.1.0",
"utf8": "^2.1.1"
}
},
"react-native-image-picker": { "react-native-image-picker": {
"version": "0.26.7", "version": "0.26.7",
"resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-0.26.7.tgz", "resolved": "https://registry.npmjs.org/react-native-image-picker/-/react-native-image-picker-0.26.7.tgz",
@@ -6682,6 +6696,11 @@
} }
} }
}, },
"utf8": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz",
"integrity": "sha1-H6DZJw6b6FDZsFAn9jUZv0ZFfZY="
},
"util-deprecate": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@@ -25,6 +25,7 @@
"react": "^16.3.2", "react": "^16.3.2",
"react-form-binder": "^2.0.0", "react-form-binder": "^2.0.0",
"react-native": "^0.55.4", "react-native": "^0.55.4",
"react-native-fs": "^2.9.12",
"react-native-image-picker": "^0.26.7", "react-native-image-picker": "^0.26.7",
"react-native-iphone-x-helper": "^1.0.3", "react-native-iphone-x-helper": "^1.0.3",
"react-native-keyboard-spacer": "^0.4.1", "react-native-keyboard-spacer": "^0.4.1",

View File

@@ -3,6 +3,7 @@ import io from "socket.io-client"
import { AsyncStorage } from "react-native" import { AsyncStorage } from "react-native"
import autobind from "autobind-decorator" import autobind from "autobind-decorator"
import { config } from "./config" import { config } from "./config"
import RNFS from "react-native-fs"
const authTokenKeyName = "AuthToken" const authTokenKeyName = "AuthToken"
const backendKeyName = "Backend" const backendKeyName = "Backend"
@@ -209,17 +210,15 @@ class API extends EventEmitter {
headers.set("Authorization", "Bearer " + this.token) headers.set("Authorization", "Bearer " + this.token)
} }
if (method === "POST" || method === "PUT") { if (method === "POST" || method === "PUT") {
if (requestOptions.raw) { if (requestOptions.binary) {
const isBase64 = requestOptions.raw.base64 const { isBase64, offset } = requestOptions.binary
headers.set( headers.set(
"Content-Type", "Content-Type",
isBase64 ? "application/base64" : "application/octet-stream" isBase64 ? "application/base64" : "application/octet-stream"
) )
headers.set("Content-Length", requestOptions.raw.length) headers.set("Content-Length", requestBody.length)
headers.set( headers.set("Content-Range", `byte ${offset}`)
"Content-Range",
(isBase64 ? "base64" : "byte") + " " + requestOptions.raw.offset
)
fetchOptions.body = requestBody fetchOptions.body = requestBody
} else { } else {
headers.set("Content-Type", "application/json") headers.set("Content-Type", "application/json")
@@ -236,7 +235,7 @@ class API extends EventEmitter {
.then((res) => { .then((res) => {
return Promise.all([ return Promise.all([
Promise.resolve(res), Promise.resolve(res),
requestOptions.raw && method === "GET" ? res.blob() : res.json(), requestOptions.binary && method === "GET" ? res.blob() : res.json(),
]) ])
}) })
.then((arr) => { .then((arr) => {
@@ -429,21 +428,27 @@ class API extends EventEmitter {
return promise return promise
} }
upload(data, progressCallback) { upload(path, progressCallback) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const chunkSize = 32 * 1024 const chunkSize = 32 * 1024
const uploadSize = data.length let uploadSize = 0
const numberOfChunks = Math.ceil(uploadSize / chunkSize) let numberOfChunks = 0
let chunk = 0 let chunk = 0
let uploadId = null let uploadId = null
const uploadNextChunk = () => { const uploadNextChunk = () => {
const start = chunk * chunkSize const offset = chunk * chunkSize
const end = Math.min(uploadSize, start + chunkSize) const length = Math.min(chunkSize, uploadSize - offset)
this.post("/assets/upload/" + uploadId, data.slice(start, end), { RNFS.read(path, length, offset, "base64")
raw: { base64: true, length: chunkSize, offset: start }, .then((data) => {
}) return this.post("/assets/upload/" + uploadId, data, {
binary: {
isBase64: true,
offset,
},
})
})
.then((uploadData) => { .then((uploadData) => {
chunk++ chunk++
if (progressCallback && !progressCallback(uploadData)) { if (progressCallback && !progressCallback(uploadData)) {
@@ -459,12 +464,18 @@ class API extends EventEmitter {
}) })
} }
this.post("/assets/upload", { RNFS.stat(path)
uploadSize, .then((stat) => {
contentType: "image/jpeg", uploadSize = stat.size
chunkContentType: "application/base64", numberOfChunks = Math.ceil(uploadSize / chunkSize)
numberOfChunks,
}) return this.post("/assets/upload", {
uploadSize,
contentType: "image/jpeg",
chunkContentType: "application/base64",
numberOfChunks,
})
})
.then((uploadData) => { .then((uploadData) => {
uploadId = uploadData.uploadId uploadId = uploadData.uploadId
uploadNextChunk() uploadNextChunk()

View File

@@ -62,6 +62,10 @@ class WorkItemSceneAR extends React.Component {
return this.arScene return this.arScene
.performARHitTestWithRay(orientation.forward) .performARHitTestWithRay(orientation.forward)
.then((results) => { .then((results) => {
if (!results) {
return
}
const forward = orientation.forward const forward = orientation.forward
const position = orientation.position const position = orientation.position
// Default position is just one meter in front of the user. // Default position is just one meter in front of the user.
@@ -69,9 +73,6 @@ class WorkItemSceneAR extends React.Component {
[forward[0] * 1.0, forward[1] * 1.0, forward[2]] * 1.0 [forward[0] * 1.0, forward[1] * 1.0, forward[2]] * 1.0
let hitResultPosition = null let hitResultPosition = null
console.log(orientation)
console.log(results)
// Filter the hit test results based on the position. // Filter the hit test results based on the position.
for (var i = 0; i < results.length; i++) { for (var i = 0; i < results.length; i++) {
let result = results[i] let result = results[i]
@@ -109,9 +110,7 @@ class WorkItemSceneAR extends React.Component {
this.updateInitialRotation() this.updateInitialRotation()
}, 200) }, 200)
}) })
.catch((err) => { .catch((err) => {})
console.log(err)
})
}) })
} }

View File

@@ -66,6 +66,7 @@ export class Activity extends React.Component {
isValid: (r, v) => v !== "", isValid: (r, v) => v !== "",
}, },
photos: { photos: {
initValue: [],
isValid: (r, v) => v && v.length > 0, isValid: (r, v) => v && v.length > 0,
}, },
status: { status: {

View File

@@ -47,8 +47,6 @@ export class Home extends React.Component {
workItemDistance: -1, workItemDistance: -1,
} }
this.watchId = null
if (Platform.OS !== "ios") { if (Platform.OS !== "ios") {
ensurePermissions( ensurePermissions(
[ [
@@ -130,13 +128,6 @@ export class Home extends React.Component {
}) })
} }
componentWillUnmount() {
if (this.watchId) {
navigator.geolocation.clearWatch(this.watchId)
this.watchId = null
}
}
@autobind @autobind
handleMessageDismiss() { handleMessageDismiss() {
this.setState({ messageModal: null }) this.setState({ messageModal: null })
@@ -255,6 +246,17 @@ export class Home extends React.Component {
this.setState({ showWorkItems: !this.state.showWorkItems }) this.setState({ showWorkItems: !this.state.showWorkItems })
} }
@autobind
handleCalloutPress(workItem) {
if (api.loggedInUser.administrator) {
this.props.history.push(
`/arviewer?workItemId=${workItem._id}&workItemType=${
workItem.workItemType
}`
)
}
}
render() { render() {
const { const {
sections, sections,
@@ -310,7 +312,7 @@ export class Home extends React.Component {
: hardhatPinImage : hardhatPinImage
} }
onPress={(e) => this.handleMarkerPress(e, index)}> onPress={(e) => this.handleMarkerPress(e, index)}>
<Callout> <Callout onPress={() => this.handleCalloutPress(workItem)}>
<View> <View>
<Text> <Text>
{pad(workItem.ticketNumber, 4) + {pad(workItem.ticketNumber, 4) +

View File

@@ -29,6 +29,7 @@ import { api } from "../API"
import "url-search-params-polyfill" import "url-search-params-polyfill"
import { config } from "../config" import { config } from "../config"
import { workItemTypeEnum, formatLatLng, parseLatLng } from "../util" import { workItemTypeEnum, formatLatLng, parseLatLng } from "../util"
import PropTypes from "prop-types"
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@@ -50,6 +51,10 @@ const styles = StyleSheet.create({
}) })
export class WorkItem extends React.Component { export class WorkItem extends React.Component {
static propTypes = {
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}
static bindings = { static bindings = {
header: { header: {
noValue: true, noValue: true,
@@ -65,6 +70,7 @@ export class WorkItem extends React.Component {
isReadOnly: true, isReadOnly: true,
}, },
photos: { photos: {
initValue: [],
isValid: (r, v) => v && v.length > 0, isValid: (r, v) => v && v.length > 0,
}, },
details: { details: {
@@ -270,6 +276,13 @@ export class WorkItem extends React.Component {
this.setState({ progressModal: null }) this.setState({ progressModal: null })
} }
@autobind
handleAddActivity() {
if (this.history) {
this.history.push(`/activity?workItemId=${this.binder._id}`)
}
}
render() { render() {
const { const {
binder, binder,
@@ -360,6 +373,26 @@ export class WorkItem extends React.Component {
onUploadProgress={this.handleUploadProgress} onUploadProgress={this.handleUploadProgress}
/> />
</View> </View>
{api.loggedInUser.administrator &&
binder._id && (
<View style={styles.panel}>
<TouchableOpacity
onPress={this.handleAddActivity}
style={{
alignSelf: "center",
backgroundColor: "blue",
justifyContent: "center",
paddingHorizontal: 10,
height: 40,
width: "100%",
backgroundColor: "#3BB0FD",
}}>
<Text style={{ alignSelf: "center", color: "black" }}>
Add Activity
</Text>
</TouchableOpacity>
</View>
)}
{isIphoneX ? <View style={{ height: 30, width: "100%" }} /> : null} {isIphoneX ? <View style={{ height: 30, width: "100%" }} /> : null}
</ScrollView> </ScrollView>
<ProgressModal <ProgressModal

View File

@@ -53,9 +53,10 @@ export class BoundPhotoPanel extends Component {
ImagePicker.showImagePicker( ImagePicker.showImagePicker(
{ {
title: "Select Photo", title: "Select Photo",
noData: true,
storageOptions: { storageOptions: {
skipBackup: true, skipBackup: true,
path: "photos", path: "deighton",
}, },
}, },
(response) => { (response) => {
@@ -64,7 +65,7 @@ export class BoundPhotoPanel extends Component {
onUploadStarted() onUploadStarted()
} }
api api
.upload(response.data, this.props.onUploadProgress) .upload(response.path, this.props.onUploadProgress)
.then((uploadData) => { .then((uploadData) => {
if (onUploadEnded) { if (onUploadEnded) {
onUploadEnded(true, uploadData) onUploadEnded(true, uploadData)
@@ -74,7 +75,7 @@ export class BoundPhotoPanel extends Component {
if (binder) { if (binder) {
const value = binder.getFieldValue(name) const value = binder.getFieldValue(name)
let newValue = value.slice(0) let newValue = typeof value === "array" ? value.slice(0) : []
newValue[index] = uploadData.assetId newValue[index] = uploadData.assetId

View File

@@ -48,7 +48,6 @@
"redis": "^2.7.1", "redis": "^2.7.1",
"redis-rstream": "^0.1.3", "redis-rstream": "^0.1.3",
"regexp-pattern": "^1.0.4", "regexp-pattern": "^1.0.4",
"safe-buffer": "^5.1.1",
"socket.io": "^2.0.3", "socket.io": "^2.0.3",
"tmp-promise": "^1.0.4", "tmp-promise": "^1.0.4",
"urlsafe-base64": "^1.0.0", "urlsafe-base64": "^1.0.0",

View File

@@ -6,12 +6,10 @@ import path from "path"
import util from "util" import util from "util"
import config from "config" import config from "config"
import autobind from "autobind-decorator" import autobind from "autobind-decorator"
import Buffer from "safe-buffer"
import B64 from "b64"
import { PassThrough } from "stream" import { PassThrough } from "stream"
import { catchAll } from "." import { catchAll } from "."
function pipeToGridFS(readable, writable, decoder) { function pipeToGridFS(readable, writeable) {
const promise = new Promise((resolve, reject) => { const promise = new Promise((resolve, reject) => {
readable.on("error", (error) => { readable.on("error", (error) => {
reject(error) reject(error)
@@ -23,13 +21,13 @@ function pipeToGridFS(readable, writable, decoder) {
resolve(file) resolve(file)
}) })
}) })
readable.pipe(decoder).pipe(writeable) readable.pipe(writeable)
return promise return promise
} }
@autobind @autobind
export class AssetRoutes { export class AssetRoutes {
static rangeRegex = /^(byte|base64) (\d+)/ static rangeRegex = /^byte (\d+)/
constructor(container) { constructor(container) {
const app = container.app const app = container.app
@@ -73,13 +71,13 @@ export class AssetRoutes {
assetId = assetId.slice(0, extIndex) assetId = assetId.slice(0, extIndex)
} }
const cursor = await this.db.gridfs.findOne({ _id: assetId }) const cursor = await this.db.gridfs.find({ _id: assetId })
const file = await cursor.next()
if (!cursor) { if (!file) {
throw createError.NotFound(`Asset ${assetId} was not found`) throw createError.NotFound(`Asset ${assetId} was not found`)
} }
const file = cursor.next()
const ifNoneMatch = req.get("If-None-Match") const ifNoneMatch = req.get("If-None-Match")
if (ifNoneMatch && ifNoneMatch === file.md5) { if (ifNoneMatch && ifNoneMatch === file.md5) {
@@ -138,7 +136,7 @@ export class AssetRoutes {
chunkContentType !== "application/base64" chunkContentType !== "application/base64"
) { ) {
throw createError.BadRequest( throw createError.BadRequest(
"chunkContentType must be application/octet-stream or application/base64" "chunkContentType must be 'application/octet-stream' or 'application/base64'"
) )
} }
} else { } else {
@@ -175,58 +173,47 @@ export class AssetRoutes {
throw createError.BadRequest(`Bad upload id ${uploadId}`) throw createError.BadRequest(`Bad upload id ${uploadId}`)
} }
if (!contentType.startsWith(uploadData.chunkContentType)) {
throw createError.BadRequest(
`Content-Type ${contentType} does not match chunk type ${
uploadData.chunkContentType
}`
)
}
if (parseInt(contentLength, 10) !== req.body.length) {
throw createError.BadRequest(
"Must supply Content-Length header matching length of request body"
)
}
let match = contentRange.match(AssetRoutes.rangeRegex)
if (!match || match.length !== 3) {
throw createError.BadRequest(
"Content-Range header must be supplied and of form '[byte|base64] <offset>'"
)
}
const [, contentOffsetUnit, contentOffset] = match
if (
(uploadData.chunkContentType === "application/octet-stream" &&
contentOffsetUnit !== "byte") ||
(uploadData.chunkContentType === "application/base64" &&
contentOffsetUnit !== "base64")
) {
throw createError.BadRequest(
`Content-Range offset unit must be ${
uploadData.chunkContentType === "application/base64"
? "base64"
: "byte"
}`
)
}
let offset = Number.parseInt(contentOffset)
if (offset < 0 || offset + req.body.length > uploadData.uploadSize) {
throw createError.BadRequest(
`Illegal Content-Range ${contentOffsetType} ${contentOffset} and Content-Length ${contentLength} for upload size ${
uploadData.uploadSize
}`
)
}
try { try {
if (!contentType.startsWith(uploadData.chunkContentType)) {
throw createError.BadRequest(
`Content-Type ${contentType} does not match chunk type ${
uploadData.chunkContentType
}`
)
}
if (parseInt(contentLength, 10) !== req.body.length) {
throw createError.BadRequest(
"Must supply Content-Length header matching length of request body"
)
}
let match = contentRange.match(AssetRoutes.rangeRegex)
if (!match || match.length !== 2) {
throw createError.BadRequest(
"Content-Range header must be supplied and of form 'byte <offset>'"
)
}
const [, contentOffset] = match
let offset = Number.parseInt(contentOffset)
const data =
uploadData.chunkContentType === "application/base64"
? Buffer.from(req.body, "base64")
: req.body
if (offset < 0 || offset + data.length > uploadData.uploadSize) {
throw createError.BadRequest(
`Illegal Content-Range 'byte ${contentOffset}' and Content-Length ${contentLength} for upload size ${
uploadData.uploadSize
}`
)
}
const [, uploadedChunks] = await Promise.all([ const [, uploadedChunks] = await Promise.all([
this.rs.setrangeAsync(uploadDataId, offset, req.body), this.rs.setrangeAsync(uploadDataId, offset, data),
this.rs.incrAsync(uploadCountId), this.rs.incrAsync(uploadCountId),
]) ])
const chunkInfo = { const chunkInfo = {
@@ -242,11 +229,7 @@ export class AssetRoutes {
{ contentType: uploadData.contentType } { contentType: uploadData.contentType }
) )
const decoder = const file = await pipeToGridFS(readable, writeable)
uploadData.chunkContentType === "application/base64"
? new B64.Decoder()
: new PassThrough()
const file = await pipeToGridFS(readable, writeable, decoder)
await Promise.all([ await Promise.all([
this.rs.del(uploadId), this.rs.del(uploadId),
@@ -275,7 +258,6 @@ export class AssetRoutes {
this.rs.del(uploadId) this.rs.del(uploadId)
this.rs.del(uploadCountId) this.rs.del(uploadCountId)
this.rs.del(uploadDataId) this.rs.del(uploadDataId)
this.log.error(error.message)
throw error throw error
} }
} }