Skip to content

Conversation

@philikon
Copy link
Contributor

With this in place, it's possible to upload a picture from the CameraRoll to Parse, for instance:

xhr = new XMLHttpRequest();
xhr.onload = function() {
  data = JSON.parse(xhr.responseText);
  var parseFile = new Parse.File(data.name);
  parseFile._url = data.url;
  callback(parseFile);
};
xhr.setRequestHeader('X-Parse-Application-Id', appID);
xhr.setRequestHeader('X-Parse-JavaScript-Key', appKey);
xhr.open('POST', 'https://api.parse.com/1/files/image.jpg');
// assetURI as provided e.g. by the CameraRoll API
xhr.send(new NativeFile(assetURI));

@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label May 20, 2015
@ide
Copy link
Contributor

ide commented May 20, 2015

cc @lwansbrough, you might find this interesting

@lwansbrough
Copy link
Contributor

@ide Very interesting! I will definitely have a use for this in my own app. But it's going to need file protocol support before it can be used by the camera module.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we add more things in RCTImageLoader we'll have to add it here too which kind of sucks as they are going to diverge. Is there anyway we can avoid listing those here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the very least we could have an [RCTImageLoader canLoadURI:uri] helper, so that these prefixes remain an implementation detail of RCTImageLoader. At some point in the future I imagine we'll have other kinds of URIs (e.g. file:///) with other ways of resolving them, at which point we might want to abstract resolving URIs altogether...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

canLoadURI works with me

@vjeux
Copy link
Contributor

vjeux commented May 22, 2015

// assetURI as provided e.g. by the CameraRoll API
xhr.send(new NativeFile(assetURI));

Can we make CameraRoll vend NativeFile objects instead? This seems like a sucky API to have to require the developer to wrap the assetURI in a NativeFile before sending it.

Also, we absolutely want to conserve as many metadata as possible from the files than just the URI. Maybe what you can do instead of having a NativeFile tag is to check if it's an object and has a uri attribute, then it's considered to be an image and you use RCTLoadImage instead of sending the object.

I'm not a big fan of using instanceof to check if it's a good type, I much prefer duck typing, this way it can be serialized and unserialized.

@vjeux
Copy link
Contributor

vjeux commented May 22, 2015

Ok, so my proposal for you is:

  • You don't introduce NativeFile
  • In sendImpl, you special case typeof data === 'object' && data.uri and send the URI down
  • In the obj-c implementation, if there's an URI then you just feed it to RCTLoadImage without any prefix check

It should make the implementation simpler. Tell me what you think

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also what's going on with this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we need to add Libraries/Image/ to the header search path so we can #import "RCTImageLoader.h"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make sure it also works with our buck rules inside of fb

@philikon
Copy link
Contributor Author

Can we make CameraRoll vend NativeFile objects instead?

Yes, I think we should... eventually. Currently the API mimics GraphQL APIs, which seems oddly FB-centric. I think we should return something more basic, and then build the GraphQL part around it using JS.

Also, we absolutely want to conserve as many metadata as possible from the files than just the URI.

Like what? I can only think of the Content-Type, and the Blob/File API has provisions for that.

I'm not a big fan of using instanceof to check if it's a good type, I much prefer duck typing, this way it can be serialized and unserialized.

I understand the desire to duck-type. I'd be ok with this, though in terms of the documentation, I think it'd be good to make NativeFile -- actually I had a rethink and want to call it NativeResource -- appear as a logical extension of the Blob/File objects we know from the web.

@vjeux
Copy link
Contributor

vjeux commented May 22, 2015

Like what? I can only think of the Content-Type, and the Blob/File API has provisions for that.

Width and height, aspect ratio, multiple versions of the same file @2x, @3x. Also, one thing that I really want to support is representing a slice of an image. {uri: 'image.jpg', crop_rect: [0, 0.5, 0, 0.5]}. This would be useful for image spriting.

@philikon
Copy link
Contributor Author

Width and height, aspect ratio, multiple versions of the same file @2x, @3x.

It seems you're conflating system image assets used in the app with camera roll assets.

Also, one thing that I really want to support is representing a slice of an image. {uri: 'image.jpg', crop_rect: [0, 0.5, 0, 0.5]}. This would be useful for image spriting.

Again, this seems useful for image assets used by the app. If you want a photo (from camera roll or the web or wherever) cropped or other transformed (like Ads Manager app), you're going to have to perform that operation in native code, which leaves you with a new URI to a new image asset, which you can then pass to XHR to upload it.

@nicklockwood
Copy link
Contributor

@philikon iOS supports displaying a sub-region of an image at the view level without having to actually chop up the image. It would be more efficient to do it that way for most purposes, although that would limit the use of the cropped image to being displayed inside an tag.

@philikon
Copy link
Contributor Author

I'm ok with making the interface that <Image source={...}> eats the same as that of a native resource that XHR.send() eats, so long as we're super explicit about it. We don't need a NativeResource class, but at least a flow type. Duck typing is great, until you forget the contracts and end up with faulty assumptions (and thus bugs) somewhere deep in the code. (I speak from a fairly recent experience here, ahem.)

@vjeux
Copy link
Contributor

vjeux commented May 22, 2015

Also, one thing that we could do is to make non-destructive transforms. {uri: 'baseImage', transforms: ['blur:20%', 'instagram-filter#5']} and don't save the temporary images. Same we could have resize, rotate...

And, eventually when you actually want to upload it, you are doing all the processing and send it.

I don't think that we want to dissociate sending images vs displaying images.

@philikon
Copy link
Contributor Author

I don't think that we want to dissociate sending images vs displaying images.

Fair enough. Let's take it one step at a time. I'll get rid of NativeFile.

@nicklockwood
Copy link
Contributor

Incidentally, I've been working on expanding the implementation of [RCTConvert data:] to make it easier to construct multipart mime uploads. I was thinking that instead of just string data, it could support a structure something like this:

[
    {text: 'Hello World'},
    {base64: 'asdasdsdadawdda'}
    {text: 'Some More text'},
    {uri: 'file://someLocalFileData'},
    {uri: 'assets-library:someImageTag'}
]

So then you could construct an entire request body on the JS side, either passing the data yourself or referencing data known to the native side by using tags/uris. This might be a more flexible solution than what you're planning with the separate dataURI.

The only problem is that it might be tricky to make the NSData blob construction asynchronous, due to the design of RCTConvert, in which case those uris might be less generally useful than they would be in your design (although most of the uris you'd want to access would be local, and the construction would happen on a background thread anyway, so it might not be too bad).

@nicklockwood
Copy link
Contributor

The other thing I wanted to do was basically unify the concept of a source (in the sense of <Image source={...}/>) and a query (in the sense of RCTDataManager) into something with a structure like:

{
    uri: someURL,
    method: GET/POST/etc (optional, defaults to GET)
    body: someData (optional, of the form described above),
    headers: someHTTPHeaders (optional)
}

This would then be processed into an NSURLRequest, which could then be used to retrieve any sort of data in any situation, whether it's loading an image, making an XHR request, setting a WebView target, etc.

The source dictionary could also include whatever other data you wanted which would be handled by the receiver, so for image sources it might have width/height and cropping information, for RCTDataManager it might include a queryType, etc.

@philikon
Copy link
Contributor Author

Incidentally, I've been working on expanding the implementation of [RCTConvert data:] to make it easier to construct multipart mime uploads.

Yes, that is exactly my goal, I've already started work on a FormData polyfill so that existing code that constructs multipart requests from a mix of native files and primitive values would Just Work(tm) on React Native. My thoughts regarding the data structure were fairly similar, though I wanted to make the specifics somewhat closer to what we're using already for the FB graph internally. Or we can change our internal "API" to match whatever we come up with here, just so long as we don't invent something that's 99% the same and 1% different.

@rt2zz
Copy link

rt2zz commented May 25, 2015

It seems to me that the analogy between uploading native files and form input files (as in HTML) is weak. Instead of stuffing file handling into the same method (i.e. xhr.send) it would be easier and more explicit to add a new method such as xhr.attachFiles. Granted modifying the xhr api sucks, and is probably a no deal.

in which case, perhaps this functionality should live elsewhere, not in react-native core.

@vjeux
Copy link
Contributor

vjeux commented May 25, 2015

@facebook-github-bot import

@facebook-github-bot
Copy link
Contributor

Thanks for importing. If you are an FB employee go to https://our.intern.facebook.com/intern/opensource/github/pull_request/897906616934455/int_phab to review.

@vjeux
Copy link
Contributor

vjeux commented May 25, 2015

@rt2zz I'm not sure why you don't think that xhr.send(data) is the right place. If you look at the API, the browser already overloaded .send to support sending many types of data: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest

The interesting one is send(Blob) which is the way you send an actual file. Which is exactly what we are doing here.

tadeuzagallo pushed a commit to tadeuzagallo/react-native that referenced this pull request May 28, 2015
Summary:
With this in place, it's possible to upload a picture from the `CameraRoll` to Parse, for instance:

    xhr = new XMLHttpRequest();
    xhr.onload = function() {
      data = JSON.parse(xhr.responseText);
      var parseFile = new Parse.File(data.name);
      parseFile._url = data.url;
      callback(parseFile);
    };
    xhr.setRequestHeader('X-Parse-Application-Id', appID);
    xhr.setRequestHeader('X-Parse-JavaScript-Key', appKey);
    xhr.open('POST', 'https://api.parse.com/1/files/image.jpg');
    // assetURI as provided e.g. by the CameraRoll API
    xhr.send(new NativeFile(assetURI));

Closes facebook#1357
Github Author: Philipp von Weitershausen <[email protected]>

Test Plan: Imported from GitHub, without a `Test Plan:` line.
@philikon philikon closed this in 4273af9 May 28, 2015
@zubru
Copy link

zubru commented Jun 12, 2015

Could you provide some quick example how to correctly setup FormData and use it with fetch? I'm trying to use it but getting error Unsupported BodyInit type in support.FormData.

@nicklockwood
Copy link
Contributor

Phil has added a nice upload example to the XHRExample in UIExplorer. It should be included in the next update, but here's the code if you want to paste it into XHRExample.js yourself.

/**
 * The examples provided by Facebook are for non-commercial testing and
 * evaluation purposes only.
 *
 * Facebook reserves all rights not expressly granted.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
 * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
 * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * @flow
 */
'use strict';

var React = require('react-native');
var {
  AlertIOS,
  CameraRoll,
  Image,
  LinkingIOS,
  PixelRatio,
  ProgressViewIOS,
  StyleSheet,
  Text,
  TextInput,
  TouchableHighlight,
  View,
} = React;

class Downloader extends React.Component {

  xhr: XMLHttpRequest;
  cancelled: boolean;

  constructor(props) {
    super(props);
    this.cancelled = false;
    this.state = {
      downloading: false,
      contentSize: 1,
      downloaded: 0,
    };
  }

  download() {
    this.xhr && this.xhr.abort();

    var xhr = this.xhr || new XMLHttpRequest();
    xhr.onreadystatechange = () => {
      if (xhr.readyState === xhr.HEADERS_RECEIVED) {
        var contentSize = parseInt(xhr.getResponseHeader('Content-Length'), 10);
        this.setState({
          contentSize: contentSize,
          downloaded: 0,
        });
      } else if (xhr.readyState === xhr.LOADING) {
        this.setState({
          downloaded: xhr.responseText.length,
        });
      } else if (xhr.readyState === xhr.DONE) {
        this.setState({
          downloading: false,
        });
        if (this.cancelled) {
          this.cancelled = false;
          return;
        }
        if (xhr.status === 200) {
          alert('Download complete!');
        } else if (xhr.status !== 0) {
          alert('Error: Server returned HTTP status of ' + xhr.status + ' ' + xhr.responseText);
        } else {
          alert('Error: ' + xhr.responseText);
        }
      }
    };
    xhr.open('GET', 'http://www.gutenberg.org/cache/epub/100/pg100.txt');
    xhr.send();
    this.xhr = xhr;

    this.setState({downloading: true});
  }

  componentWillUnmount() {
    this.cancelled = true;
    this.xhr && this.xhr.abort();
  }

  render() {
    var button = this.state.downloading ? (
      <View style={styles.wrapper}>
        <View style={styles.button}>
          <Text>Downloading...</Text>
        </View>
      </View>
    ) : (
      <TouchableHighlight
        style={styles.wrapper}
        onPress={this.download.bind(this)}>
        <View style={styles.button}>
         <Text>Download 5MB Text File</Text>
        </View>
      </TouchableHighlight>
    );

    return (
      <View>
        {button}
        <ProgressViewIOS progress={(this.state.downloaded / this.state.contentSize)}/>
      </View>
    );
  }
}

var PAGE_SIZE = 20;

class FormUploader extends React.Component {

  _isMounted: boolean;
  _fetchRandomPhoto: () => void;
  _addTextParam: () => void;
  _upload: () => void;

  constructor(props) {
    super(props);
    this.state = {
      isUploading: false,
      randomPhoto: null,
      textParams: [],
    };
    this._isMounted = true;
    this._fetchRandomPhoto = this._fetchRandomPhoto.bind(this);
    this._addTextParam = this._addTextParam.bind(this);
    this._upload = this._upload.bind(this);

    this._fetchRandomPhoto();
  }

  _fetchRandomPhoto() {
    CameraRoll.getPhotos(
      {first: PAGE_SIZE},
      (data) => {
        console.log('isMounted', this._isMounted);
        if (!this._isMounted) {
          return;
        }
        var edges = data.edges;
        var edge = edges[Math.floor(Math.random() * edges.length)];
        var randomPhoto = edge && edge.node && edge.node.image;
        if (randomPhoto) {
          this.setState({randomPhoto});
        }
      },
      (error) => undefined
    );
  }

  _addTextParam() {
    var textParams = this.state.textParams;
    textParams.push({name: '', value: ''});
    this.setState({textParams});
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  _onTextParamNameChange(index, text) {
    var textParams = this.state.textParams;
    textParams[index].name = text;
    this.setState({textParams});
  }

  _onTextParamValueChange(index, text) {
    var textParams = this.state.textParams;
    textParams[index].value = text;
    this.setState({textParams});
  }

  _upload() {
    var xhr = new XMLHttpRequest();
    xhr.open('POST', 'http://posttestserver.com/post.php');
    xhr.onload = () => {
      this.setState({isUploading: false});
      if (xhr.status !== 200) {
        AlertIOS.alert(
          'Upload failed',
          'Expected HTTP 200 OK response, got ' + xhr.status
        );
        return;
      }
      if (!xhr.responseText) {
        AlertIOS.alert(
          'Upload failed',
          'No response payload.'
        );
        return;
      }
      var index = xhr.responseText.indexOf('http://www.posttestserver.com/');
      if (index === -1) {
        AlertIOS.alert(
          'Upload failed',
          'Invalid response payload.'
        );
        return;
      }
      var url = xhr.responseText.slice(index).split('\n')[0];
      LinkingIOS.openURL(url);
    };
    var formdata = new FormData();
    if (this.state.randomPhoto) {
      formdata.append('image', {...this.state.randomPhoto, name: 'image.jpg'});
    }
    this.state.textParams.forEach(
      (param) => formdata.append(param.name, param.value)
    );
    xhr.send(formdata);
    this.setState({isUploading: true});
  }

  render() {
    var image = null;
    if (this.state.randomPhoto) {
      image = (
        <Image
          source={this.state.randomPhoto}
          style={styles.randomPhoto}
        />
      );
    }
    var textItems = this.state.textParams.map((item, index) => (
      <View style={styles.paramRow}>
        <TextInput
          autoCapitalize="none"
          autoCorrect={false}
          onChangeText={this._onTextParamNameChange.bind(this, index)}
          placeholder="name..."
          style={styles.textInput}
        />
        <Text style={styles.equalSign}>=</Text>
        <TextInput
          autoCapitalize="none"
          autoCorrect={false}
          onChangeText={this._onTextParamValueChange.bind(this, index)}
          placeholder="value..."
          style={styles.textInput}
        />
      </View>
    ));
    var uploadButtonLabel = this.state.isUploading ? 'Uploading...' : 'Upload';
    var uploadButton = (
      <View style={styles.uploadButtonBox}>
        <Text style={styles.uploadButtonLabel}>{uploadButtonLabel}</Text>
      </View>
    );
    if (!this.state.isUploading) {
      uploadButton = (
        <TouchableHighlight onPress={this._upload}>
          {uploadButton}
        </TouchableHighlight>
      );
    }
    return (
      <View>
        <View style={[styles.paramRow, styles.photoRow]}>
          <Text style={styles.photoLabel}>
            Random photo from your library
            (<Text style={styles.textButton} onPress={this._fetchRandomPhoto}>
              update
            </Text>)
          </Text>
          {image}
        </View>
        {textItems}
        <View>
          <Text
            style={[styles.textButton, styles.addTextParamButton]}
            onPress={this._addTextParam}>
            Add a text param
          </Text>
        </View>
        <View style={styles.uploadButton}>
          {uploadButton}
        </View>
      </View>
    );
  }
}


exports.framework = 'React';
exports.title = 'XMLHttpRequest';
exports.description = 'XMLHttpRequest';
exports.examples = [{
  title: 'File Download',
  render() {
    return <Downloader/>;
  }
}, {
  title: 'multipart/form-data Upload',
  render() {
    return <FormUploader/>;
  }
}];

var styles = StyleSheet.create({
  wrapper: {
    borderRadius: 5,
    marginBottom: 5,
  },
  button: {
    backgroundColor: '#eeeeee',
    padding: 8,
  },
  paramRow: {
    flexDirection: 'row',
    paddingVertical: 8,
    alignItems: 'center',
    borderBottomWidth: 1 / PixelRatio.get(),
    borderBottomColor: 'grey',
  },
  photoLabel: {
    flex: 1,
  },
  randomPhoto: {
    width: 50,
    height: 50,
  },
  textButton: {
    color: 'blue',
  },
  addTextParamButton: {
    marginTop: 8,
  },
  textInput: {
    flex: 1,
    borderRadius: 3,
    borderColor: 'grey',
    borderWidth: 1,
    height: 30,
    paddingLeft: 8,
  },
  equalSign: {
    paddingHorizontal: 4,
  },
  uploadButton: {
    marginTop: 16,
  },
  uploadButtonBox: {
    flex: 1,
    paddingVertical: 12,
    alignItems: 'center',
    backgroundColor: 'blue',
    borderRadius: 4,
  },
  uploadButtonLabel: {
    color: 'white',
    fontSize: 16,
    fontWeight: '500',
  },
});

@ashleydw
Copy link

Did this get implemented into Android?

I have this working on ios fine, but in Android, the request made misses the image. I see the commits in this pull refer to Libraries/Network/XMLHttpRequest.ios.js and related ios files.

@philikon
Copy link
Contributor Author

Yes, I added the same machinery to Android in a separate commit. It lives here: https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java

If it's not working for you, I suggest you open a separate issue and describe your problem in detail (including how exactly you're getting the image's URI and adding it to the request.) Thanks!

@skyride99
Copy link

@philikon Is it possible to upload through a uri pointing to the image. I have resized the image and have the uri. Something like
formdata.append('image', {..., pic.uri });

@nicklockwood
Copy link
Contributor

@skyride99 yes, that should work (although you'd need the uri key I think):

formdata.append('image', {uri: pic.uri});

@skyride99
Copy link

Thanks
this is on Android and RN 17.
the uri is "file:///storage/emulated/0/Pictures/72621376-6710-43cd-8d02-d35d13a68273.jpg"

formdata.append('image', {uri: pic.uri});

but it returns status of 0.

I can upload the base64 like this

formdata.append('file', pic.data);

Really need to upload the binary data not sure how to do it with xhr....

@ashleydw
Copy link

@skyride99 I recently put some code in this issue (make sure you leave out the Content-Type header; read the comments). Maybe this will help you #5308

@skyride99
Copy link

Thanks much but different error now.
400
"POST requires exactly one file upload per request."

@ashleydw
Copy link

That seems like a server error, you should ask on stackoverflow for help

@skyride99
Copy link

@ashleydw You are correct. The server wants 'file' always file. Not 'image'
it has to be
formData.append('file', {uri: pic.uri, type: 'image/jpg', name: 'seventh.jpg'});

mganandraj pushed a commit to mganandraj/react-native that referenced this pull request Aug 28, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.