How to read a .tar file containing parquets on S3 as dataframes in Spark?

Viewed 765

I need to load a .tar file on S3 that contains multiple parquets with different schema using Scala/Spark. Ideally I'd like to read one of these parquets into Spark dataframe. I tried to get the s3 object and then convert to a tar input stream using org.apache.commons.compress.archivers.tar.TarArchiveInputStream and it was able to creat the tar input stream but failed to read the tar entries.

val s3client: AmazonS3 = AmazonS3ClientBuilder.
      standard().
      withCredentials(new InstanceProfileCredentialsProvider()).
      withRegion(my_region).
      build();

val tarFile = s3client.getObject(my_bucket, my_tar_file)
val tarInputStream = new TarArchiveInputStream(tarFile.getObjectContent)
tarInputStream.getNextTarEntry() <-- error thrown in this line

Error:

java.io.IOException: Error detected parsing the header
  at org.apache.commons.compress.archivers.tar.TarArchiveInputStream.getNextTarEntry(TarArchiveInputStream.java:240)
  ... 52 elided
Caused by: java.lang.IllegalArgumentException: Invalid byte 48 at offset 7 in '00755{NUL}00' len=8
  at org.apache.commons.compress.archivers.tar.TarUtils.parseOctal(TarUtils.java:127)
  at org.apache.commons.compress.archivers.tar.TarUtils.parseOctalOrBinary(TarUtils.java:171)
  at org.apache.commons.compress.archivers.tar.TarArchiveEntry.parseTarHeader(TarArchiveEntry.java:935)
  at org.apache.commons.compress.archivers.tar.TarArchiveEntry.parseTarHeader(TarArchiveEntry.java:924)
  at org.apache.commons.compress.archivers.tar.TarArchiveEntry.<init>(TarArchiveEntry.java:328)
  at org.apache.commons.compress.archivers.tar.TarArchiveInputStream.getNextTarEntry(TarArchiveInputStream.java:238)

Does anyone have the knowledge of the proper way of extract a partial of tar file on s3 in Spark?

3 Answers

Follow this example. I hope you are using tar.gz

AWSCredentials credentials = new BasicAWSCredentials("accessKey", "secretKey");
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
AmazonS3 s3Client = AmazonS3ClientBuilder.standard().withRegion(Regions.US_EAST_1).withCredentials(credentialsProvider).build();
S3Object object = s3Client.getObject("bucketname", "file.tar.gz");
S3ObjectInputStream objectContent = object.getObjectContent();

TarArchiveInputStream tarInputStream = new TarArchiveInputStream(new GZIPInputStream(objectContent));
TarArchiveEntry currentEntry;
while((currentEntry = tarInputStream.getNextTarEntry()) != null) {
    if(currentEntry.getName().equals("1/foo.bar") && currentEntry.isFile()) {
        FileOutputStream entryOs = new FileOutputStream("foo.bar");
        IOUtils.copy(tarInputStream, entryOs);
        entryOs.close();
        break;
    }
}
objectContent.abort();  // Warning at this line
tarInputStream.close(); // warning at this line

scala equivalent is

    val credentials: AWSCredentials =
      new BasicAWSCredentials("accessKey", "secretKey")
    val credentialsProvider: AWSCredentialsProvider =
      new AWSStaticCredentialsProvider(credentials)
    val s3Client: AmazonS3 = AmazonS3ClientBuilder
      .standard()
      .withRegion(Regions.US_EAST_1)
      .withCredentials(credentialsProvider)
      .build()
    val s3object: S3Object = s3Client.getObject("bucketname", "file.tar.gz")
    val objectContent: S3ObjectInputStream = s3object.getObjectContent
    val tarInputStream: TarArchiveInputStream = new TarArchiveInputStream(
      new GZIPInputStream(objectContent))
    var currentEntry: TarArchiveEntry = null
    while ((currentEntry = tarInputStream.getNextTarEntry) != null) 
if (currentEntry.getName ==("1/foo.bar") && currentEntry.isFile) {
      val entryOs: FileOutputStream = new FileOutputStream("foo.bar")
      IOUtils.copy(tarInputStream, entryOs)
      entryOs.close()
    }
    objectContent.abort()
    tarInputStream.close()
  }

Update :

since you are using only tar not gzip

so you have to read like this...

val tarInputStream = new TarArchiveInputStream(new FileInputStream(
    tarFile.getObjectContent))

In your case you are passing object as a InputStream. My suggestion is to pass it as a GzipInputstream, then read entries:

val tarInputStream = new TarArchiveInputStream(tarFile.getObjectContent)

val tarInputStream = new TarArchiveInputStream(new GZIPInputStream(tarFile))
val entry: TarArchiveEntry = readEntries(tarInputStream)
def readEntries(tarInputStream: TarArchiveInputStream): TarArchiveEntry = {
  var currentEntry = Option(tarInputStream.getNextTarEntry())
  // you can use functional approach with foldLeft, reduce or something else or while loop
  // implementation details here
}

You can find how to use TarArchiveInputStream usage here

You can use GetObjectRequest to create an S3Object

val s3FullObject: S3Object = s3client.getObject(new GetObjectRequest(s3Bucket, s3TarPath))
val tis = new TarArchiveInputStream(s3FullObject.getObjectContent)
var entry: TarArchiveEntry = tis.getNextTarEntry
Related