Download files via FTP on Android - android

I need to make a connection with a local FTP protocol between a computer (server) and my Android device (client). This should download files (images, OBJ,...) to be used in the Android Unity app scene. I've used WWW class to create this connection and it works fine in the Unity player run in another computer as client. Once I've exported the same scene as Android apk it didn't work (I'm sure that FTP connection is stable and it works because I'm able to access to the files from the browser). Does anybody know if there is another way or there are problems in my code to use the FTP protocol on Android Unity app? (the client doesn't need any authorisation and the authentication is anonymous) Here is the code I use to download one image inside the scene and render it as a sprite.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net;
using System.IO;
public class ClientFTP : MonoBehaviour
{
public UnityEngine.UI.Image label;
IEnumerator Start ()
{
// Create the connection and whait until it is established
string url = ("ftp://192.168.10.11/prova.png");
WWW ftpconnection = new WWW (url);
yield return ftpconnection;
// Download the image and render it as a texture
Texture2D tex = new Texture2D (250, 192);
ftpconnection.LoadImageIntoTexture (tex);
// Assign the texture to a new sprite
Sprite s = Sprite.Create (tex, new Rect (0, 0, 250f, 192f), new Vector2 (0.5f, 0.5f), 300);
label.preserveAspect = true;
label.sprite = s;
}
}

Why use FTP if you don't need Credential to access the files? You can just place the files in your server then access them with the WWW or UnityWebRequest API.
To answer your FTP question, WWW is not meant to be used with the FTP protocol. This is what the FtpWebRequest API is used for.
Below is a sample of FtpWebRequest.
private byte[] downloadWithFTP(string ftpUrl, string savePath = "", string userName = "", string password = "")
{
FtpWebRequest request = (FtpWebRequest)WebRequest.Create(new Uri(ftpUrl));
//request.Proxy = null;
request.UsePassive = true;
request.UseBinary = true;
request.KeepAlive = true;
//If username or password is NOT null then use Credential
if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(password))
{
request.Credentials = new NetworkCredential(userName, password);
}
request.Method = WebRequestMethods.Ftp.DownloadFile;
//If savePath is NOT null, we want to save the file to path
//If path is null, we just want to return the file as array
if (!string.IsNullOrEmpty(savePath))
{
downloadAndSave(request.GetResponse(), savePath);
return null;
}
else
{
return downloadAsbyteArray(request.GetResponse());
}
}
byte[] downloadAsbyteArray(WebResponse request)
{
using (Stream input = request.GetResponseStream())
{
byte[] buffer = new byte[16 * 1024];
using (MemoryStream ms = new MemoryStream())
{
int read;
while (input.CanRead && (read = input.Read(buffer, 0, buffer.Length)) > 0)
{
ms.Write(buffer, 0, read);
}
return ms.ToArray();
}
}
}
void downloadAndSave(WebResponse request, string savePath)
{
Stream reader = request.GetResponseStream();
//Create Directory if it does not exist
if (!Directory.Exists(Path.GetDirectoryName(savePath)))
{
Directory.CreateDirectory(Path.GetDirectoryName(savePath));
}
FileStream fileStream = new FileStream(savePath, FileMode.Create);
int bytesRead = 0;
byte[] buffer = new byte[2048];
while (true)
{
bytesRead = reader.Read(buffer, 0, buffer.Length);
if (bytesRead == 0)
break;
fileStream.Write(buffer, 0, bytesRead);
}
fileStream.Close();
}
Usage:
Download and save(No Credential):
string path = Path.Combine(Application.persistentDataPath, "FTP Files");
path = Path.Combine(path, "data.png");
downloadWithFTP("ftp://yourUrl.com/yourFile", path);
Download and save(With Credential):
string path = Path.Combine(Application.persistentDataPath, "FTP Files");
path = Path.Combine(path, "data.png");
downloadWithFTP("ftp://yourUrl.com/yourFile", path, "UserName", "Password");
Download Only (No Credential):
string path = Path.Combine(Application.persistentDataPath, "FTP Files");
path = Path.Combine(path, "data.png");
byte[] yourImage = downloadWithFTP("ftp://yourUrl.com/yourFile", "");
//Convert to Sprite
Texture2D tex = new Texture2D(250, 192);
tex.LoadImage(yourImage);
Sprite s = Sprite.Create(tex, new Rect(0, 0, texture2D.width, texture2D.height), new Vector2(0.5f, 0.5f));
Download Only (With Credential):
string path = Path.Combine(Application.persistentDataPath, "FTP Files");
path = Path.Combine(path, "data.png");
byte[] yourImage = downloadWithFTP("ftp://yourUrl.com/yourFile", "", "UserName", "Password");
//Convert to Sprite
Texture2D tex = new Texture2D(250, 192);
tex.LoadImage(yourImage);
Sprite s = Sprite.Create(tex, new Rect(0, 0, texture2D.width, texture2D.height), new Vector2(0.5f, 0.5f));

public boolean ftpDownloadVideo() {
String desFilePath = (Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS)).toString(); ///xet trong moi truong SD card////
String srcFilePath = "/detect_disease/rice_detect_disease_2019_09_24__10_07_08.avi";
boolean status = false;
Log.d(TAG, "2");
try {
Log.e("downloadFTP login : ", "Success");
OutputStream desFileStream = new BufferedOutputStream( new FileOutputStream(desFilePath));
Log.d(TAG,"FileOutputStream");
status = mFTPClient.retrieveFile(srcFilePath, desFileStream);
Log.d(TAG, "4");
desFileStream.close();
Log.e("downloadFTP status : ", "" + status);
Log.d(TAG, "5");
return status;
} catch (Exception e) {
Log.d(TAG, "download failed");
}
return status;
}

Related

Xamarin.Android: Decrypted Files Randomly Being Corrupted

I have an application that encrypts files for storage purposes and decrypts them for uploading to our server. For the most part it works fine and the decrypted files we receive are good files.
However, on a semi-regular basis (one or two files out of a few hundred every few days) the decrypted files are corrupt. It's possible that the original files were corrupt as well and the issue was not in the encrypt/decrypt process, but this has occurred to different users who presumably would have made sure that the files weren't corrupt to begin with, but I don't know how to verify this as I do not have direct contact with our users.
I want to make sure that my code is not the source of the problem, or if there is something I can do to check that the files were safely encrypted/decrypted in the application to make sure all the files we receive are good.
What is strange is that I have an ExceptionManager class that sends me any exceptions that occur, and I do not receive any exceptions when these corrupt files get processed and uploaded, so I have no way of knowing what went wrong.
This is my Encrypt code:
static string aesTransform = "AES/CBC/PKCS5Padding"
public static bool EncryptFile(Context context, string fileLocation, ref string encryptedFileLocation)
{
bool success;
try
{
Log logger = new Log(context);
logger.Debug("starting encryption");
if (File.Exists(fileLocation))
{
encryptedFileLocation = (encryptedFileLocation ?? fileLocation) + ".aes";
using (FileStream fInput = File.Open(fileLocation, FileMode.Open))
using (FileStream fOutput = File.Open(encryptedFileLocation, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
byte[] keyBytes = Encoding.UTF8.GetBytes(encryptKey);
SecretKeySpec key = new SecretKeySpec(keyBytes, 0, keyBytes.Length, "AES");
Cipher cipher = Cipher.GetInstance(aesTransform);
cipher.Init(CipherMode.EncryptMode, key, new IvParameterSpec(new byte[cipher.BlockSize]));
CipherOutputStream cos = new CipherOutputStream(fOutput, cipher);
byte[] buffer = new byte[1024];
int numBytesWritten = 0;
while ((numBytesWritten = fInput.Read(buffer, 0, buffer.Length)) > 0)
{
cos.Write(buffer, 0, numBytesWritten);
}
cos.Flush();
cos.Close();
}
logger.WriteLog(Log.LogType.Misc, fileLocation + " encrypted");
Delete(context, fileLocation);
}
success = true;
}
catch(Exception ex)
{
if (File.Exists(fileLocation) && File.Exists(encryptedFileLocation))
{
Delete(context, encryptedFileLocation);
}
if(encryptedFileLocation.EndsWith(".aes"))
{
encryptedFileLocation = encryptedFileLocation.Remove(encryptedFileLocation.ToUpper().LastIndexOf(".AES"));
}
ex.Data.Add("fileLocation", fileLocation);
ex.Data.Add("encryptedFileLocation", encryptedFileLocation);
ExceptionManager.ManageException(context, ex);
success = false;
}
return success;
}
And this is my Decrypt Code:
static string aesTransform = "AES/CBC/PKCS5Padding"
public static bool DecryptFile(Context context, string fileLocation, ref string decryptedFileLocation)
{
bool decrypted = false;
try
{
if (File.Exists(fileLocation))
{
if(fileLocation.ToUpper().EndsWith(".AES"))
{
decryptedFileLocation = fileLocation.Remove(fileLocation.ToUpper().LastIndexOf(".AES"));
}
else
{
decryptedFileLocation = fileLocation;
}
using (FileStream fInput = File.Open(fileLocation, FileMode.Open))
using (FileStream fOutput = File.Open(decryptedFileLocation, FileMode.OpenOrCreate))
{
FileInfo info = new FileInfo(fileLocation);
byte[] keyBytes = Encoding.UTF8.GetBytes(encryptKey);
SecretKeySpec key = new SecretKeySpec(keyBytes, 0, keyBytes.Length, "AES");
Cipher cipher = Cipher.GetInstance(aesTransform);
cipher.Init(CipherMode.DecryptMode, key, new IvParameterSpec(new byte[cipher.BlockSize]));
CipherInputStream cis = new CipherInputStream(fInput, cipher);
byte[] buffer = new byte[1024];
int numBytesWritten = 0;
while ((numBytesWritten = cis.Read(buffer, 0, buffer.Length)) > 0)
{
fOutput.Write(buffer, 0, numBytesWritten);
}
cis.Close();
decrypted = true;
Log logger = new Log(context);
logger.WriteLog(Log.LogType.Misc, fileLocation + " decrypted");
}
}
}
catch (Exception ex)
{
if (File.Exists(fileLocation) && File.Exists(decryptedFileLocation))
{
Delete(context, decryptedFileLocation);
}
decryptedFileLocation = fileLocation;
ex.Data.Add("fileLocation", fileLocation);
ExceptionManager.ManageException(context, ex);
decrypted = false;
}
return decrypted;
}
The encrypt key is identical for both Encrypt and Decrypt methods, both referencing a static string declared earlier in this class, so the keys match.

Android upload to network drive(samba share) performance issue

i have a problem when I upload a picture of 100kb to samba share with JCIFS from my tablet, it takes about 10-20 minutes (before I changed my buffer from 1024 to 20971520 it took almost 6 hours) but it does not give any effect anymore to increase it
it is not the connection issue as i had tested it with ES File where it Uploaded my picture immediately
private class MyCopy extends AsyncTask<String, String, String> {
String z = "";
String username = "", password = "", servername = "", filestocopy = "";
#Override
protected void onPreExecute() {
username = edtusername.getText().toString();
password = edtpassword.getText().toString();
servername = "smb://" + edtservername.getText().toString();
filestocopy = editdir.getText().toString();
}
protected String doInBackground(String... params) {
// String buffer;
// buffer = setingPreferences.getString("buffer", "");
File file = new File(filestocopy);
String filename = file.getName();
NtlmPasswordAuthentication auth1 = new NtlmPasswordAuthentication(
servername, username, password);
try {
SmbFile sfile = new SmbFile(servername + "/" + filename, auth1);
if (!sfile.exists())
sfile.createNewFile();
sfile.connect();
InputStream in = new FileInputStream(file);
SmbFileOutputStream sfos = new SmbFileOutputStream(sfile);
byte[] buf = new byte[20971520]; //(parseInt(buffer))
int len;
while ((len = in.read(buf)) > 0){
sfos.write(buf, 0, len);
}
in.close();
sfos.close();
z = "File copied successfully";
} catch (Exception ex) {
z = z + " " + ex.getMessage().toString();
}
return z;
}
}
The buffer size shouldn't make a noticeable difference, but it definitely shouldn't be 20M. Use something like 4k instead.
Are you sure it's the actual file transfer that is taking so long? There's no reason 100k should take more than a few seconds at the most. Have you tried putting log statements between each step, including before and after the authentication calls, createNewFile(), and connect() to check if those are the bottleneck?
Also, I believe you should be copying bytes while the read length is >= 0 instead of strictly > 0, since -1 signals the end of the stream, not 0.
Did you try
new SmbFile("username:password#server/")
instead of using NTLM? It can also be a DNS issue, so do try
jcifs.Config.setProperty("resolveOrder", "DNS");
If neither works, you might want to try BufferedOutputStream with your SmbFileOutputStream.

How to download a database and integrate into the app

I'm trying to download a database from a server and then use it in my app. My initial situation is that I've got a database in my assets folder and I am trying to download the whole database from a server when the user is clicking a button.
I think I have to use the onUpgrade method but I don't know how I can download the database and then use it or and load it into the assetfolder.
You need to brake the problem into smaller issues.
Make sure that the server side is able to provide you with a web-service with which you'll do the actual download.
It would be ideal if your server would zip your sqlite database in order for you to minimize traffic.
After you have downloaded the db on your device, you'll have to unzip it, if you have taken this approach.
When you get to the point of having the db downloaded and unzipped it's just a matter of connecting to a sqlite db and Android provides api in this regards.
Of-course, all of the above needs to be done in a multithreaded environment, you could also take into account using an Android service.
This is an example of downloading a file from a server, it should help you get an idea, it's not too generic, it works in my environment.
private void handleSelectedItemDownload(final String downloadItem) {
try {
int bufferLength;
long downloadedSize = 0;
long downloadProgressBytes = 0;
final byte[] buffer = new byte[12 * 163840];
this.url = new URL(String.format(AppConstants.SERVICE_BASE_URL + AppConstants.MEDIA_IMAGE_REQUEST_URL_PATH + downloadItem));
InputStream inputStream = this.getStream(downloadedSize);
downloadedSize = this.chunkSize;
if (downloadedSize > 0) {
this.applicationFolder = downloadItem.equals("pictures") ? Utils.getThumbNailsFolder() : Utils.getApplicationFolder();
final File mediaFolder = new File(this.applicationFolder);
mediaFolder.mkdirs();
final File file = new File(mediaFolder, String.format(File.separator + downloadItem + ".zip"));
final FileOutputStream fileOutput = new FileOutputStream(file);
this.actionStatusProgressBar.setProgress(0);
final Message downloadStartMessage = new Message();
downloadStartMessage.what = ACTION_STARTED;
this.actionMessageHandler.sendMessage(downloadStartMessage);
int progressBarMax = (int) (this.totalDownloadSize / 1024);
this.actionStatusProgressBar.setMax(progressBarMax);
while (chunkSize != 0) {
while ((bufferLength = inputStream.read(buffer)) > 0) {
fileOutput.write(buffer, 0, bufferLength);
downloadProgressBytes += bufferLength;
final Message currentProgressMessage = new Message();
currentProgressMessage.arg1 = (int) (downloadProgressBytes / 1024);
currentProgressMessage.what = ACTION_PROGRESS_VALUE;
this.actionMessageHandler.sendMessage(currentProgressMessage);
}
inputStream.close();
this.urlConnection.disconnect();
inputStream = this.getStream(downloadedSize);
downloadedSize += chunkSize;
}
fileOutput.close();
}
this.urlConnection.disconnect();
this.totalDownloadSize = 0;
} catch (MalformedURLException e) {
Log.e("DOWNLOAD_ERROR", e.getMessage() + e.toString());
this.errorEncountered = true;
this.actionType = IDataActionListener.ACTION_TYPE.ACTION_FAILED;
this.actionMessageHandler.sendEmptyMessage(ERROR_ENCOUNTERED);
} catch (IOException e) {
Log.e("DOWNLOAD_ERROR", e.getMessage() + e.toString());
this.errorEncountered = true;
this.actionType = IDataActionListener.ACTION_TYPE.ACTION_FAILED;
this.actionMessageHandler.sendEmptyMessage(ERROR_ENCOUNTERED);
} catch (Exception e) {
Log.e("DOWNLOAD_ERROR", e.getMessage() + e.toString());
this.errorEncountered = true;
this.actionType = IDataActionListener.ACTION_TYPE.ACTION_FAILED;
this.actionMessageHandler.sendEmptyMessage(ERROR_ENCOUNTERED);
}
}
private InputStream getStream(final long downloadedSize) throws IOException {
this.urlConnection = (HttpURLConnection) this.url.openConnection();
this.urlConnection.setRequestMethod("GET");
this.urlConnection.addRequestProperty("DOWNLOADED_SIZE", String.valueOf(downloadedSize));
final InputStream inputStream = this.urlConnection.getInputStream();
this.chunkSize = this.urlConnection.getContentLength();
if (!Utils.stringIsNullOrEmpty(this.urlConnection.getHeaderField("TOTAL_SIZE"))) {
if (this.totalDownloadSize == 0) {
this.totalDownloadSize = Long.parseLong(this.urlConnection.getHeaderField("TOTAL_SIZE"));
}
}
return inputStream;
}
Afterwards, when you have downloaded your db file, you can just create a SQLiteDatabase object:
final File dbFile = new File(filePath);
if (dbFile.exists()) {
this.localDatabase = SQLiteDatabase.openDatabase(filePath, null, SQLiteDatabase.OPEN_READWRITE);
this.localDatabase.setLocale(Locale.getDefault());
}
I still stick to the opinion that you should also give zipping a thought, you reduce a LOT the traffic and the time spent over network.

Downloading 3gp Video from Youtube Rtsp URL on Android

I'm trying to download the 3gp video file from Youtube's rtsp URL and save it to my external storage, but it appears that the method I have only works with HTTP URL.
I received this warning:
09-02 10:32:40.877: WARN/System.err(7988): java.net.MalformedURLException: Unknown protocol: rtsp
My download method is the following:
public static void downloadFromURL(String url, File cacheDir, String fileName) throws IOException {
long startTime = System.currentTimeMillis();
String TAG = "DL";
if (url != null) {
if (fileName.contains("/"))
fileName = fileName.replace("/", "-");
url = url.replace(" ", "%20");
URL mUrl = new URL(url);
URLConnection ucon = mUrl.openConnection();
ucon.setReadTimeout(TIMEOUT_CONNECTION);
ucon.setConnectTimeout(TIMEOUT_SOCKET);
File f = new File(cacheDir, fileName);
InputStream is = ucon.getInputStream();
BufferedInputStream inStream = new BufferedInputStream(is, BUFFER_SIZE);
FileOutputStream outStream = new FileOutputStream(f);
byte[] buff = new byte[BUFFER_SIZE];
int len;
while ((len = inStream.read(buff)) != -1) {
outStream.write(buff, 0, len);
}
// clean up
outStream.flush();
outStream.close();
inStream.close();
android.util.Log.d(TAG, "Download completed in " + ((System.currentTimeMillis() - startTime) / 1000) + " sec");
}
}
FYI I test it on my Nexus 7 with Wi-Fi connection.
Let me know if anybody has the solution to the problem I'm having.
UPDATE
Anyway, I found these Objective-C snippets which do the request I need, but I'm clueless to turn it into Java codes:
NSString *urlString = #"http://www.youtube.com/get_video_info?&video_id=7ubt8AWa7SU"; //7ubt8AWa7SU gylfmQgtMJc
NSURL *infoUrl = [NSURL URLWithString:urlString];
NSError *error = NULL;
NSString *info = [NSString stringWithContentsOfURL:infoUrl encoding:NSUTF8StringEncoding error:&error];
if (info == nil)
{
NSLog(#"Error: %#", [error description]);
return;
}
NSArray *urlComponents = [info componentsSeparatedByString:#"&"];
NSArray *itemComponents;
NSString *urlEncodedFmtStreamMap = NULL;
for (NSString *item in urlComponents)
{
itemComponents = [item componentsSeparatedByString:#"="];
if (itemComponents)
{
NSString *first = [itemComponents objectAtIndex:0];
if ([first isEqualToString: #"url_encoded_fmt_stream_map"])
{
urlEncodedFmtStreamMap = [itemComponents objectAtIndex:1];
break;
}
}
}
NSString *type = NULL;
NSString *urlSeg1 = NULL;
NSString *sig = NULL;
if (urlEncodedFmtStreamMap)
{
NSArray *formats = [[urlEncodedFmtStreamMap stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding] componentsSeparatedByString:#","];
for (NSString *item in formats)
{
type = NULL;
urlSeg1 = NULL;
sig = NULL;
NSArray *pairs = [item componentsSeparatedByString:#"&"];
for (NSString *pair in pairs)
{
NSArray *varComps = [pair componentsSeparatedByString:#"="];
NSString *varName = [varComps objectAtIndex: 0];
NSString *varValue = [[varComps objectAtIndex: 1] stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
if ([varName isEqualToString: #"type"])
{
NSArray *typeSegments = [varValue componentsSeparatedByString: #";"];
type = [typeSegments objectAtIndex:0];
}
else if ([varName isEqualToString: #"url"])
{
urlSeg1 = [varValue stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
}
else if ([varName isEqualToString: #"sig"])
{
sig = varValue;
}
}
if ([type isEqualToString:#"video/mp4"] && urlSeg1 && sig)
{
self.videoUrl = [[urlSeg1 stringByAppendingString: #"&signature="] stringByAppendingString: sig];
break;
}
}
}
I don't think there's an easy way of doing it. Getting an RTSP stream is not as simple as a HTTP GET request. The video and audio data packets are sent as RTP, essentially UDP packets with extra data, and then you have RTSP and RTCP (which you can ignore) for server/client communication.
I would recommend looking into the MediaPlayer and Stagefright source code, especially where they set up different data sources and see if you can extract parts of the RTSP handshaking and the setup of the RTP session.

Image invalid sent by ANDROID to C# TCPListener

I'm send image from android to a TCPClient in C# but when receive image, this is invalid image.
I have created a method to validate stream but no have image header.
Anyone can help me?
Here my ANDROID code:
try {
Socket sockTcp = new Socket(EnderecoSoftclinic, PORTATCP);
while(sockTcp.isClosed()){
Toast.makeText(contexto, "Tentando conectar ao SoftClinic", Toast.LENGTH_SHORT).show();
sockTcp.connect(sockTcp.getRemoteSocketAddress(), PORTATCP);
}
OutputStream outputStream = sockTcp.getOutputStream();
outputStream.write(imagem,0,imagem.length);
outputStream.flush();
sockTcp.close();
} catch (IOException e) {
e.printStackTrace();
}
And my receive C# Code:
TcpListener tcpReceiver = new TcpListener(IPAddress.Any, 2553);
tcpReceiver.Start(1);
while (!finalizado)
{
TcpClient android = tcpReceiver.AcceptTcpClient();
NetworkStream stream = android.GetStream();
byte[] buffer = null;
buffer = new byte[Convert.ToInt32(tamanho)];
if (stream.CanRead)
{
while (stream.Read(buffer, 0, buffer.Length) > 0) {}
}
if (buffer == null || buffer.Length == 0)
{ return; }
MemoryStream streamImg = new MemoryStream();
streamImg.Write(buffer, 0, buffer.Length);
if (!IsValidImage(streamImg))
{
finalizado = true;
return;
}
if (File.Exists("C:/teste.bmp"))
{
File.Delete("C:/teste.bmp");
}
FileStream streamWriter = new FileStream("C:/teste.bmp", FileMode.CreateNew, FileAccess.Write);
streamWriter.Write(buffer, 0, buffer.Length);
streamWriter.Flush();
streamWriter.Close();
try
{
Image imagemGerada = Image.FromStream(streamImg);
if (AoRetornarDadosEquipamento != null)
{
AoRetornarDadosEquipamento(imagemGerada);
}
}
catch (Exception ex)
{
Debug.Print(ex.Message);
}
stream.Close();
android.Close();
finalizado = true;
}
tcpReceiver.Stop();
I see one problem.
The MemoryStream (streamImg) is read once while you save it to the file.
Then you do not rewind it and try to load the Image (hence reading past the end of file!). Use
streamImg.Seek(0, SeekOrigin.Begin)
after the line
streamWriter.Close();
This can do the job.
The other way is to recreate the streamImg (this will do practically the same).
I did it..
In more easily way i just get the NetworkStream in my c# program and CopyTo new Stream.
And done.
Thanks...........
Just need this
NetworkStream stream = android.GetStream();
MemoryStream streamImg = new MemoryStream();
stream.copyto(streamimg);

Categories

Resources