React Native ile Büyük Boyuttaki Dosyayı Node.js Sunucusuna Yüklemek

nodejsreactreact-native

6 ay önce 0 yorum

React Native ile multipart/form-data tipinde bir HTTP isteği atılabilmesi, büyük dosyaların sunucuya http kullanılarak upload edilebilmesini kolaylaştırıyor. Node.js'de eğer express kullanıyorsanız işiniz kolay. Çünkü multer adındaki kütüphanesi tam da bu işimize yarayacak.

Her şeyden önce Node.js file upload uygulamasını yazalım.

Herhangi bir klasör oluşturup içinde index.js ve package.json dosyalarını oluşturmak için hızlıca npm init --ff kullanıyorum. Bir şeyleri yazmaya başlamadan önce express'i kuralım.

yarn add express multer

bağımlılıkları kurduğumuza göre index.js dosyası aşağıdaki gibi yazabiliriz:

const path = require('path');
const express = require('express');
const multer = require('multer');

const app = express();

// medya yüklenebilecek bir middleware
const mediaUpload = multer({
    dest: path.join(require('os').tmpdir(), 'nodejs-upload-example'),
});

// middleware'i ekliyor sadece tek dosya için
app.put('/upload', mediaUpload.single('file'), (req, res) => {
    // req içerisinde file yüklenen dosyayı belirtiyor
    const file = req.file;

    // bu dosyanın son durumunu nereye yüklendiğini response içerisinde verelim
    res.send(file);
});

app.listen(8080, () => {
    console.log('http://localhost:8080/ üzerinde uygulama çalışmakta');
});

Komut satırından node index.js şeklinde çalıştırabiliriz.

Buraya bir istekte bulunmak için curl veya postman kullanabiliriz. Daha açıklayıcı olması açısından postman kullanırsak isteğimiz şu şekilde olacaktır:

path değerinde yazan yere dosyamız upload olmuş durumda. Şu anda bu bizim için yeterli, React Native üzerinden upload etmeyi deneyelim.

Dizinimi değiştirmeden basit bir React Native uygulaması oluşturmak için aşağıdaki komutu kullanıyorum:

react-native init FileUploadExample

React Native uygulaması oluştuğuna göre, dosya yükleyebilmemiz için bir dosya seçici kullanalım. Hem android hem de iOS için React Native Image Picker kullanabiliriz diye düşünüyorum. Hem resim hem video seçilebiliyor. Onu kurmak için:

yarn add react-native-image-picker

# bundan sonra sadece iOS için
cd ios/ && pod install

Yukarıdaki işlemler tam olarak doğru olmayabilir, dokümanından kurulum yaparsanız daha iyi olacaktır. Çünkü resim seçme işlemi için kullanıcıdan izin istemeniz gerekebilir.

Bu işlemlerden sonra App.js dosyasını aşağıdaki gibi değiştirelim.

...
import ImagePicker from 'react-native-image-picker';

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      error: null,
      loading: false,
      response: null,
    };

    this.pickerOptions = {
      title: 'Yüklemek İçin Resim Seç',
      noData: true, // dosyanın binary verisi gelmemesi için yapıldı
      storageOptions: {
        skipBackup: true,
        path: 'images',
      },
    };

    this.launchCamera = this.launchCamera.bind(this);
    this.launchImageLibrary = this.launchImageLibrary.bind(this);
    this.uploadFile = this.uploadFile.bind(this);
  }

  launchCamera() {
    ImagePicker.launchCamera(this.pickerOptions, this.uploadFile);
  }

  launchImageLibrary() {
    ImagePicker.launchImageLibrary(this.pickerOptions, this.uploadFile);
  }

  uploadFile(response) {
    if (response.didCancel || response.error || response.customButton)
      return;

    this.setState({
      error: null,
      loading: true,
    });

    // form data türündeki dosya verisi
    const file = {
      uri : response.uri, // dosyanın yolu
      type: response.type, // dosyanın mimeType değeri
      name: response.fileName, // dosyanın ismi
    };

    // multipart/form-data şeklinde gönderilebilmesi için FormData şeklinde tanımlanıyor
    let formdata = new FormData();
    formdata.append('file', file);

    // Sunucuya yükleme işlemi buradan oluyor
    fetch('http://localhost:8080/upload', {
      method: 'PUT',
      body: formdata,
      headers: {
        'Content-Type': 'multipart/form-data', // Bu değeri vermek önemli!
        'Accept': 'application/json',
      },
    })
    .then(res => res.text())
    .then(res => {
      this.setState({
        response: res,
        loading: false,
      });
    })
    .catch(err => {
      this.setState({
        error: err,
        loading: false,
      });
    });
  }

  render() {
    return (
      <SafeAreaView>
        <Header />
        <View style={styles.body}>
          <TouchableOpacity
            onPress={this.launchCamera}
            style={styles.button}>
            <Text style={styles.buttonText}>Kamerayı Açıp Video/Resim Yükle</Text>
          </TouchableOpacity>
          <TouchableOpacity
            onPress={this.launchImageLibrary}
            style={styles.button}>
            <Text style={styles.buttonText}>Galeriden Yükle</Text>
          </TouchableOpacity>
        </View>
        <View>
          {
            // yüklemedeyse göster
            this.state.loading ? <View>
              <ActivityIndicator size='large'/>
            </View> : null
          }

          {
            // hata varsa göster
            this.state.error ? <View>
              <Text>Hata Oluştu: {this.state.error}</Text>
            </View> : null
          }

          {
            // sonuç varsa göster
            this.state.response ? <View>
              <Text>Sonuç: {this.state.response}</Text>
            </View> : null
          }
        </View>
      </SafeAreaView>
    );
  }
}
...

Uygulamayı başlatalım ve galeriden resim seçip gönderdiğimizde neler oluyor bir bakalım.

Bunu başlattığımızda MulterError: Field value too long hatası ile karşılaştık. Bunun sebebi Node.js uygulamasındaki bir limite takılmamız. fieldSize değerine. 

Multer için fieldSize'ın değeri 1MB, insan bir düşünüyor değil mi? multer neden 1MB gibi bir limit vermiş. fieldSize değeri multipart verinin içerisinde gönderilen dosyanın adının/başlığının limiti. Peki bizim dosyamızın field değeri nasıl olur da 1MB'ı geçer? 

Bu problem dosya adından kaynaklı bir problem. iOS veya Android farketmez bir dosya oluşturulduğunda ImagePicker size varsayılan bir isim vermiyor yani response.fileName alanı boş geliyor. Dolayısıyla dosyayı göndermeden önce ismi yoksa ona bir isim vermeliyiz. Yukarıdaki kodumuzda FormData oluşturduğumuz yeri şu şekilde düzenlersek sorun çözülmüş olur:

...
    // form data türündeki dosya verisi
    const file = {
      uri : response.uri, // dosyanın yolu
      type: response.type, // dosyanın mimeType değeri
-      name: response.fileName, // dosyanın ismi
+      name: response.fileName || `auto-file-${+new Date()}`, // dosyanın ismi
    };

    // multipart/form-data şeklinde gönderilebilmesi için FormData şeklinde tanımlanıyor
    let formdata = new FormData();
    formdata.append('file', file);
...

Bu işlemi yaptığımızda dosyamız başarıyla yükleniyor:

Aslında yapacaklarımız bu kadardı. Bu koddan sonra boyutu GB'ları aşan dosyaları bile yükleyebilirsiniz.

Bu makaleyi yazmamın sebebi MulterError: Field value too long hatasının zor yoldan bulmak oldu. Öncelikle bu hatayı arattığım her platform'da fieldSize değerini artırmayı tavsiye eden yanıtları gördüm, evet dedikleri çalışıyordu ancak bir sıkıntı vardı. Bu sıkıntı memory'yi düşünmemeleri. Eğer nodejs uygulamanızı 1core 1GB ram gibi bir makinede çalıştırıyorsanız bunu düşünmelisiniz. Hatta iyi bir makineniz olsa bile düşünmelisiniz. Çünkü bir DoS saldırısında uygulamanız anında çöker. field limitini 25MB yaptınız varsayalım 1GB'da ram var ama node uygulaması, şu, bu derken 500MB'a kadar size ayrılan memory kalsa 20 kişi (20 kişiye bile gerek kalmayabilir) upload servisini meşgul etse JavaScript heap out of memory hatasını alıp servisiniz çöker.

Ben de ilk başta fieldSize değerini artırdım, sonra JavaScript heap out of memory hatasını alınca bu işin farklı bir şekilde olması gerektiğini düşündüm. npm'de busboy paketinin source kodunu okumaya başladım. Orda şu dosyada bir şey dikkatimi çekti:

...
var onData,
  onEnd;
if (contype === 'application/octet-stream' || filename !== undefined) {
// file/binary field
if (nfiles === filesLimit) {
  if (!boy.hitFilesLimit) {
    boy.hitFilesLimit = true;
    boy.emit('filesLimit');
  }
  return skipPart(part);
}
...

filename !== undefined kontrolü şunu ifade ediyor, eğer dosya adı varsa fileSize önemli. Bu if'in else bloğundaysa tam da sorunun çözümünü içeren fieldSize önemli. Yani multipart/form-data üzerinden gönderilen dosyaların isimleri yoksa o zaman bu dosyalar fieldSize değeri kadar olmalı.

Bu sorunu da çözdükten sonra memory düşük bir node uygulamasında bile binlerce kişiye file upload servisi sunulabilir.

Bu makaleye örnek olsun diye başka bir zamanda dosyaların sıkıştırılması ve tutulması konusunu içeren bir makale yapabilirim. Bahsettiğim bu makaleye ek olarak isteklerinizi buraya yorum yaparsanız içeriğini zenginleştirebilirim. Sağlıcaklar kalın, sağlıklı günler :)

Tüm kodlar: nodejs-upload-example

Düşündüklerin nedir ?

Abdurrahman Eker

(1010 Eylül 11111001100)

  • Full Stack Developer at Detaysoft Turkey/Sivas
  • İnternette Avare Kodcu
  • codewars
  • github
  • linkedin
  • superpeer
  • youtube
  • Yeni içeriklerden haberdar olmak ister misin ?