I am very new to backend development. Basically, I want to create a robust & simple application that will accept a zip file URL in the params and then download the zip file from the URL and finally extract the zip and return the bin file inside it. Note: The zip file size can range from 5MB to 150MB. I have tried doing the described operation in the following manner.
package la.sample
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.http.HttpStatusCode
import io.ktor.response.respond
import io.ktor.response.respondFile
import io.ktor.routing.get
import io.ktor.routing.routing
import java.io.*
fun Application.startServer() {
routing {
get("/get-bin") {
//Gets the AWS Url from params
val awsUrl = call.request.queryParameters.get("url") ?: "Error"
// Download the zip file from the AWS URL
val client = HttpClient()
val bytes = client.get<ByteArray>(awsUrl)
//Create a temp file on the server & write the zip file bytes into it.
val file = File(".", "data.zip")
file.writeBytes(bytes)
//Call a method to unzip the file
unzipAndReturnBinFile()?.let {
call.respondFile(it) //respond with bin file
} ?: kotlin.run{
call.respond(HttpStatusCode.InternalServerError)
}
}
}
}
fun unzipAndReturnBinFile(): File? {
var exitVal = 0
//Command shell to unzip the file
Runtime.getRuntime().exec("unzip bundle.zip -d data").let {//command shell to unzip the zip file
exitVal += it.waitFor()
}
//Check if the command executed successfully
if (exitVal == 0) {
var binFile: File? = null
//check if the extracted files contain `bin`
File("data").listFiles().forEach {
if (it.name.contains(".bin")) {
binFile = it
}
}
//return bin or null otherwise
return binFile
} else {
throw Exception("Command Shell Execution failed.")
}
}
The above codes work fine in local machine, irrespective of the Zip file size. But when it is deployed to AWS, the code breaks if the zip or the bin file is larger than 100 MB and gives a java.lang.OutOfMemoryError error. I will be very thankful if someone can suggest to me a proper way of handling large file operations in the backend with the ability to handle 100s of such concurrent calls. Thank you.
Java heap size of my remote machine is around 1 GB.
your problem is not from the unzipping procedure,
runtime exec command runs on a different process and only use of min size on the heap of the forked process to save instruction of return address.
the problem that causing the outOfMemory is in these lines
val bytes = client.get<ByteArray>(awsUrl)
val file = File(".", "data.zip")
file.writeBytes(bytes)
it will only take 6 concurrent requests of size 150Mb to finish all your Heap size.
instead of waiting for the file to fully download before saving it to the disk, you should use Stream, and then every time a chunk of data downloaded you saving it to the disk then in that way the full size of the downloaded file will never be at the RAM at the same time.
Use apache commons-io, for example :
FileUtils.copyURLToFile(URL, File)
or if you would like more control over the procedure try using Ben Noland answer
https://stackoverflow.com/a/921408/4267015
Based on #Naor's comment, I have updated the code to accept the multipart file and write every small chuck (part) to another file as soon as I get them, without storing the entire data in the memory. It has solved the issue. Below is the updated code snippet.
val file = File(".", Constant.FILE_PATH)
call.receiveMultipart().apply {
forEachPart {
if (it is PartData.FileItem) {
it.streamProvider().use { input ->
file.outputStream().buffered().use { output -> input.copyToSuspend(output) }
}
}
it.dispose
} }
Related
I am trying to write some kind of an external config file for my Xamarin.Android-application. Just .txt and this only needs to contain several strings, so no rocket science^^
e.g. (Sample Goal/Hopefully final content of the text file:)
[TestSection]
test=12345
I tried it with a tutorial and the following code:
//This code snippet is one example of writing a string to a UTF-8 text file and into the internal storage directory of an application:
public void SaveSomethingIntoExternalTextFile(string toWrite)
{
var backingFile = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal), "config.txt");
using (var writer = System.IO.File.CreateText(backingFile))
{
writer.WriteLine(toWrite);
}
}
But unfortunately this has no effect. As my only app-specific path is
Phone\Android\data\com.<company_name>.<application_name>\files
I cannot find any file there after running the code above. And even if I create a new file with the computer in the given path, name it as written (config.txt) and run the given code again, still nothing happens (so the string which's passed as parameter into the method unfortunately isn't written into that file as tried).
Just for the sake of completeness, my test application is pretty simple:
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SaveSomethingIntoExternalTextFile("[TestSection]");
SaveSomethingIntoExternalTextFile("test="+12345);
}
Can anyone maybe help? What am I doing wrong?
Would be really happy about every answer, thanks in advance and
Best regards
P.S.: If I open the by computer generated text file, even if in the file directory its displayed correctly, the heading of the file says config[1].txt, opening again config[2].txt and so on. Or does that not matter/has nothing to do with my attempt above?
If you use System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal), the path is like
/data/data/App3.App3/files/config.txt
It means that you save it in internal storage, you could not see it without root permission,if you want to see it, you can save it in external storage.
public void SaveSomethingIntoExternalTextFile(string toWrite)
{
string path = Android.App.Application.Context.GetExternalFilesDir(null).ToString();
string filepath = Path.Combine(path, "text.txt");
using (var writer = System.IO.File.CreateText(filepath))
{
writer.WriteLine(toWrite);
}
}
The path like :
/storage/emulated/0/Android/data/App3.App3/files/text.txt
Okay via Debugging, I found out that indeed he does write the value, at least I can retrieve it with the following function (I changed the type of the value to integer as in my tutorial (https://learn.microsoft.com/de-de/xamarin/android/platform/files/index#reading-or-writing-to-files-on-internal-storage ) it was integer as well):
//This code snippet provides one way to read an int value that was stored in a text file:
public int ReadSomethingFromTextFile()
{
var backingFile = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal), "config.txt");
if (backingFile == null || !System.IO.File.Exists(backingFile))
{
return 0;
}
int count = 0;
using (var reader = new StreamReader(backingFile, true))
{
string line;
while ((line = reader.ReadLine()) != null)
{
if (int.TryParse(line, out var newcount))
{
count = newcount;
}
}
}
return count;
}
Nevertheless, I cannot find the written file. Is the Path System.Environment.SpecialFolder.Personal the right one? Or does this point to root directory, where I am always seeking in some kind of user folder?
Because when looking with the debugger what's inside backingFile, it's
/data/data/com.<company_name>.<application_name>/files/config.txt,
and as written in the post above, the only app-specific path I can access via explorer is
Phone/Android/data/com.<company_name>.<application_name>/files.
I read something about having to root my phone in order to be able to access this path? Makes no sense right?
Isn't there some kind of shared folder (/path), whichs accessable not only by application/operating system but also from outside?
In my code I am prompting the user to load a json file.
I am then attempting to copy this file into an sqlite database.
Once I have the data I am then able to manipulate it as needed - but I need to get it there in the first place.
So step 1 is to get the data in.
I have progressed as far as prompting the user to navigate to the file they want - but when I try and read the file I get this error ..
ERROR: resources must reside in the root directory thus must start with a '/' character in Codename One! Invalid resource: file:///tmp/temp3257201851214246357..json
So I think that I need to copy this file to the root directory
I cannot find a link that shows me how to do this.
Here is my code so far ...
case "Import Script":
try
{
JSONParser json = new JSONParser();
if (FileChooser.isAvailable()) {
FileChooser.showOpenDialog(".json", e2-> {
String file = (String)e2.getSource();
if (file == null) {
home.add("No file was selected");
home.revalidate();
} else {
home.add("Please wait - busy importing");
home.revalidate();
String extension = null;
if (file.lastIndexOf(".") > 0) {
extension = file.substring(file.lastIndexOf(".")+1);
}
if ("json".equals(extension)) {
FileSystemStorage fs = FileSystemStorage.getInstance();
try {
InputStream fis = fs.openInputStream(file);
try(Reader r = new InputStreamReader(Display.getInstance().getResourceAsStream(getClass(), file), "UTF-8"))
{
Map<String, Object> data = json.parseJSON(r);
Result result = Result.fromContent(data);
...... I progress from here
The error is occurring on this line ...
try(Reader r = new InputStreamReader(Display.getInstance().getResourceAsStream(getClass(), file), "UTF-8"))
If I hard code a filename and manually place it in the /src folder it works ... like this ...
try(Reader r = new InputStreamReader(Display.getInstance().getResourceAsStream(getClass(), '/test.json'), "UTF-8"))
But that defeats the purpose of them selecting a file
Any help would be appreciated
Thanks
I suggest watching this video.
It explains the different ways data is stored. One of the core sources of confusion is the 3 different ways to store files:
Resources
File System
Storage
getResourceAsStream returns a read only path that's physically embedded in the jar. It's flat so all paths to getResourceAsStream must start with / and must have only one of those. I would suggest avoiding more than one . as well although this should work in theory.
The sqlite database must be stored in file system which is encapsulated as FileSystemStorage and that's really the OS native file system. But you can't store it anywhere you want you need to give the DB name to the system and it notifies you where the file is stored and that's whats explained in the code above.
I want to write entries to a log file stored in Azure file storage. I currently have this:
var log = "My log entry";
var client = _storageAccount.CreateCloudFileClient();
var share = client.GetShareReference(Config.LogShare);
share.CreateIfNotExists();
var root = share.GetRootDirectoryReference();
var logfile = root.GetFileReference("log.txt");
if (!logfile.Exists()) logfile.Create(0);
// What goes here to append to the file...?
I can see plenty of examples of how to do this with Blobs, or how to upload an entire file, but how do I just append to an existing file?
I have tried this:
var buffer = Encoding.GetEncoding("UTF-8").GetBytes(log.ToCharArray());
using (var fileStream = logfile.OpenWrite(0)) {
fileStream.Write(buffer, (int)logfile.Properties.Length, buffer.Length);
}
But then I get this error:
The remote server returned an error: (416) The range specified is invalid for the current size of the resource..
I managed to work this out myself. You just need to increase the size of the file by the number of new bytes you want to write to it, and then write the new data to that new empty space at the end of the file, like this:
var client = _storageAccount.CreateCloudFileClient();
var share = client.GetShareReference(Config.LogShare);
share.CreateIfNotExists();
var root = share.GetRootDirectoryReference();
var logfile = root.GetFileReference("log.txt");
if (!logfile.Exists()) logfile.Create(0);
var buffer = Encoding.UTF8.GetBytes($"{log}\r\n");
logfile.Resize(logfile.Properties.Length + buffer.Length);
using (var fileStream = logfile.OpenWrite(null)) {
fileStream.Seek(buffer.Length * -1, SeekOrigin.End);
fileStream.Write(buffer, 0, buffer.Length);
}
You can do this with blobs https://blogs.msdn.microsoft.com/windowsazurestorage/2015/04/13/introducing-azure-storage-append-blob/
Shame it doesn't work with files too
Azure file storage REST API doesn't support appending to an existing file. To achieve this, please mount the file share to your machine as a drive, and append to the file just like simple local files.
Actually, I don't think you really need appending functionality per your code above. You can specify the file size in CloudFile.OpenWrite() / CloudFile.Create(), or try CloudFile.UploadFromStream() instead of CloudFile.OpenWrite().
This error could also be due to multi-threaded access.
I bet if you tried to lock the file before you access it, you will not face this problem.
There are many ways to update the file.
Since you already managed to get the share, the root, the folder and the file.. Here is a portion of my code that worked for me.
if (!fileLock.IsWriteLockHeld) fileLock.EnterWriteLock();
try
{
using (var stream = new MemoryStream(content, false))
{
file.UploadFromStream(stream, null, options);
}
}
catch (Exception ex)
{
File.AppendAllText(FileName, ex.ToString());
}
finally
{
if (fileLock.IsWriteLockHeld)
fileLock.ExitWriteLock();
}
Where fileLock is declared as:
protected ReaderWriterLockSlim fileLock = new ReaderWriterLockSlim();
Having said that, I am not saying that this is the best way ever to do it.
The two things I would like you to keep in mind :
1-Lock the resource that is likely to be accessed by more than one thread (That is so common in AZURE)
2- Get familiar with asynchronous methods that Azure provides.. use them when they suit well.
Coming back to your original problem about appending to the existing file..
All the methods of the CloudFile will overwrite the existing file. Cloud Files are not for frequent writing, and they indeed impact performance if you keep writing on them frequently, add the lock impact on performance, they will be horrible.
Cloud files are meant to store big bulk of data once and for all, if you want to add another bulk you have the choice of creating another file.
Have all your data with the client till they reach some size and create an algorith to select the file name and upload them all at once.
Im programming in Titanium for Android.
I have 6 sqlite databases and i don't want to store them on the device internal memory because DB amount size is too large.
So, how can i move the sqlite files to SD card programmatically? Or how to install Dbs directly on the SD card when users installs my App from PlayStore?
PD: I try adding "PreferExternal" but this didn't fix my problem.
<manifest android:installLocation="preferExternal</manifest>
thanks in advance!
From Titanium docs of Ti.Database.open.
Open a Database on External Storage (Android)
A database, with a filename of mydb2Installed and located at the absolute path provided, is opened.
if (Ti.Platform.name === 'android' && Ti.Filesystem.isExternalStoragePresent()) {
var db2 = Ti.Database.open(Ti.Filesystem.externalStorageDirectory + 'path' + Ti.Filesystem.separator + 'to' + Ti.Filesystem.separator + 'mydb2Installed');
}
Hopefully this will do the trick.
I have no idea about titanium. But in android you can move DB by below logic:
copy your Database.db file in your projects assets folder.
In manifest file define permission shown below:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
now using code copy database file from /asset to device's external storage
For copy file use below code,
try {
// Open your local db as the input stream
InputStream myInput = myContext.getAssets().open("your database file name");
// Path to the just created empty db
String outFileName = "path of external storage/<database_file_name>";
OutputStream myOutput = new FileOutputStream(outFileName);
// transfer bytes from the inputfile to the outputfile
byte[] buffer = new byte[1024];
int length;
while ((length = myInput.read(buffer)) > 0)
{
myOutput.write(buffer, 0, length);
}
// Close the streams
myOutput.flush();
myOutput.close();
myInput.close();
}
catch (Exception e)
{
Log.e("error", e.toString());
}
According to this question : Android: use SQLite database on SD Card (not using internal Android Data Store at all)
You can use :
SQLiteDatabase.openOrCreateDatabase(String, SQLiteDatabase.CursorFactory)
Put your path as the first parameter and null as the second.
To get the path of your sdcard do :
Environment.getExternalStorageDirectory();
But note that everyone with a physical access to the device can access to the database file...
I have developed a ETL which is consuming flat files. The size of flat files varies from 250 MB - 300 MB.
It is working absoultely fine when file present in the folder. But it fails when the file is in generation mode.
Ex: This ETL package runs from 8 AM to 10 AM to check whether the file is present in the folder or not. Now, at any instance(let say 9 AM) if the file is starting generated and till now it is 10 MB. ETL start processing the file and just hang and fail after 4-5 min ( hang at script task which is reading that the file is present in the folder or not).
What is the best way to trigger SSIS package only when the file generation is completely done?
Note: I have no control over the file generation.
Add a For Loop Container with a Boolean variable bFileAccessible:
The Init expression is #bFileAccessible=False
The Eval expression is #bFileAccessible==False
Inside the For Loop Container add a Script Task with a ReadWriteVariable User::bFileAccessible and the following C# script (showing only the Main() method):
public void Main()
{
try
{
using (Stream stream = new FileStream("Path\to\your\file", FileMode.Open))
{
Dts.Variables["bFileAccessible"].Value = true;
}
}
catch
{
Dts.Variables["bFileAccessible"].Value = false;
}
Dts.TaskResult = (int)ScriptResults.Success;
}
You should also use a variable for the filename and maybe a little wait interval. For more information about the script see here.
Check the FIle modified time everytime and comapre the same with previous one....
it's not good logic but a good idea if no perfect alternative