3 yıl ö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 ?