Heinrich
Heinrich

Reputation: 398

Django serve zipped GeoDataFrame shapefile from memeory as download

I have a Django GIS-related application where users can download shp files. I have the geopandas GeoDataFrame object. I can easily convert it to a zipfile and then read the zipfile to the user when they want to download it:


from django.http import HttpResponse
import geopandas as gpd
import shapely
import os
from zipfile import ZipFile
def download_shp_zip(request):
    # just some random polygon
    geometry = shapely.geometry.MultiPolygon([
        shapely.geometry.Polygon([ (0, 0), (0, 1), (1, 1), (1, 0) ]),
        shapely.geometry.Polygon([ (2, 2), (2, 3), (3, 3), (3, 2) ]),
    ])
    # create GeoDataFrame
    gdf = gpd.GeoDataFrame(data={'geometry':geometry}, crs='epsg:4326')
    # some basename to save under
    basename = 'basename'
    #  create folder for this session
    os.mkdir(f"local_folder/{basename}")
    # export gdf to this folder
    gdf.to_file(f"local_folder/{basename}/{basename}.shp")
    # this file now contains many files. just zip them
    zipObj = ZipFile(f"local_folder/{basename}.zip", 'w')
    # zip everything in the folder to the zip
    for file in os.listdir(f"local_folder/{basename}"):
        zipObj.write(f"local_folder/{basename}/{file}")
    # create zip
    zipObj.close()
    # now delete the original files that were zipped
    shutil.rmtree(f"local_folder/{basename}")

    # now we can server the zip file to the user
    filename = f'local_folder/{basename}.zip'
    # check if file exists (just in case)
    try:
        fsock = open(filename, "rb")
    except:
        return HttpResponse(f"File '{basename}' Does Not Exist!",
            content_type='text/plain')
    # create response
    response = HttpResponse(fsock, content_type='application/zip')
    response['Content-Disposition'] = f'attachment; filename={basename}.zip'
    return response
    

This method works, but it saves the file to local storage and then serves it. However, I want to save them to Memory and then serve that to the user instead. I've seen people use os.BytesIO to do something similar. I've been playing around, but I can't quite get anything to work for what I'm looking for.

Again, I have the GeoDataFrame. I want to convert it to a shapefile and then zip it and then serve the zipped folder to the user from Memory without writing it to Local Storage.

Upvotes: 2

Views: 621

Answers (1)

Helge Schneider
Helge Schneider

Reputation: 573

It would be possible to create the file and delete it instantly after serving by using tempfile. So not the answer you were looking for but maybe still of help. According to this answer:

Any file created by tempfile will be deleted once the file handler is closed. In your case, when you exit the with statement.

from django.http import HttpResponse
import geopandas as gpd
import shapely
import os
import tempfile
from zipfile import ZipFile

def download_shp_zip(request):

    # just some random polygon
    geometry = shapely.geometry.MultiPolygon([
        shapely.geometry.Polygon([(0, 0), (0, 1), (1, 1), (1, 0)]),
        shapely.geometry.Polygon([(2, 2), (2, 3), (3, 3), (3, 2)]),
    ])
    # create GeoDataFrame
    gdf = gpd.GeoDataFrame(data={'geometry': geometry}, crs='epsg:4326')

    # some basename to save under
    basename = 'basename'

    # Convert to shapefile and serve it to the user
    with tempfile.TemporaryDirectory() as tmp_dir:

        # Export gdf as shapefile
        gdf.to_file(os.path.join(tmp_dir, f'{basename}.shp'), driver='ESRI Shapefile')

        # Zip the exported files to a single file
        tmp_zip_file_name = f'{basename}.zip'
        tmp_zip_file_path = f"{tmp_dir}/{tmp_zip_file_name}"
        tmp_zip_obj = ZipFile(tmp_zip_file_path, 'w')

        for file in os.listdir(tmp_dir):
            if file != tmp_zip_file_name:
                tmp_zip_obj.write(os.path.join(tmp_dir, file), file)

        tmp_zip_obj.close()

        # Return the file
        with open(tmp_zip_file_path, 'rb') as file:
            response = HttpResponse(file, content_type='application/force-download')
            response['Content-Disposition'] = f'attachment; filename="{tmp_zip_file_name}"'
            return response

Another option would be to use the return statement inside a try-except block and to delete the file in the finally statement:

        # Return the zip file as download response and delete it afterwards
        try:
            with open(tmp_zip_file_path, 'rb') as file:
                response = HttpResponse(file, content_type='application/force-download')
                response['Content-Disposition'] = f'attachment; filename="{tmp_zip_file_name}"'
                return response
        finally:
            os.remove(tmp_zip_file_path)

For a better user experience, you can use a loading gif until the download is starting.

Upvotes: 5

Related Questions