-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2910 from entur/otp2_ds6_Google_cloud_storage_sup…
…port Otp2 ds6 google cloud storage support
- Loading branch information
Showing
26 changed files
with
1,256 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# Google Cloud Storage - Using GCS Bucket as a OTP Data Source | ||
|
||
## Contact Info | ||
|
||
- Thomas Gran, Entur, Norway | ||
|
||
|
||
## Changelog | ||
|
||
### OTP 2.0 | ||
- Initial implementation of Google Cloud Storage | ||
|
||
## Documentation | ||
To enable this turn on `OTPFeature`. Each artifact to load or save to the cloud must be | ||
configured in build-config.json. See `StorageParameters` on how to configure artifacts. | ||
|
||
Example (build-config.json): | ||
```json | ||
{ | ||
: | ||
storage : { | ||
gcsCredentials: "/Users/alf/secret/otp-test-1234567890.json", | ||
osm : [ "gs://otp-test-bucket/a/b/northpole.pbf" ], | ||
dem : [ "gs://otp-test-bucket/a/b/northpole.dem.tif" ], | ||
gtfs: [ "gs://otp-test-bucket/a/b/gtfs.zip" ], | ||
graph: "gs://otp-test-bucket/a/b/graph.obj" | ||
buildReportDir: "gs://otp-test-bucket/a/b/np-report" | ||
} | ||
} | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
src/ext/java/org/opentripplanner/ext/datastore/gs/AbstractGsDataSource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package org.opentripplanner.ext.datastore.gs; | ||
|
||
import com.google.cloud.storage.BlobId; | ||
import org.opentripplanner.datastore.DataSource; | ||
import org.opentripplanner.datastore.FileType; | ||
|
||
abstract class AbstractGsDataSource implements DataSource { | ||
private final BlobId blobId; | ||
private final FileType type; | ||
|
||
AbstractGsDataSource(BlobId blobId, FileType type) { | ||
this.blobId = blobId; | ||
this.type = type; | ||
} | ||
|
||
BlobId blobId() { | ||
return blobId; | ||
} | ||
|
||
String bucketName() { | ||
return blobId.getBucket(); | ||
} | ||
|
||
@Override | ||
public final String name() { | ||
return blobId.getName(); | ||
} | ||
|
||
@Override | ||
public final String path() { | ||
return GsHelper.toUriString(blobId); | ||
} | ||
|
||
@Override | ||
public final FileType type() { | ||
return type; | ||
} | ||
|
||
@Override | ||
public final String toString() { | ||
return type + " " + path(); | ||
} | ||
} |
107 changes: 107 additions & 0 deletions
107
src/ext/java/org/opentripplanner/ext/datastore/gs/GsDataSourceRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
package org.opentripplanner.ext.datastore.gs; | ||
|
||
|
||
import com.google.auth.oauth2.GoogleCredentials; | ||
import com.google.cloud.storage.Blob; | ||
import com.google.cloud.storage.BlobId; | ||
import com.google.cloud.storage.Storage; | ||
import com.google.cloud.storage.StorageOptions; | ||
import org.opentripplanner.datastore.CompositeDataSource; | ||
import org.opentripplanner.datastore.DataSource; | ||
import org.opentripplanner.datastore.FileType; | ||
import org.opentripplanner.datastore.base.DataSourceRepository; | ||
import org.opentripplanner.datastore.base.ZipStreamDataSourceDecorator; | ||
|
||
import java.io.FileInputStream; | ||
import java.io.IOException; | ||
import java.net.URI; | ||
import java.util.Collections; | ||
|
||
/** | ||
* This data store uses the local file system to access in-/out- data files. | ||
*/ | ||
public class GsDataSourceRepository implements DataSourceRepository { | ||
private final String credentialsFilename; | ||
private Storage storage; | ||
|
||
public GsDataSourceRepository(String credentialsFilename) { | ||
this.credentialsFilename = credentialsFilename; | ||
} | ||
|
||
@Override | ||
public void open() { | ||
this.storage = connectToStorage(); | ||
} | ||
|
||
@Override | ||
public String description() { | ||
return "Google Cloud Storage"; | ||
} | ||
|
||
@Override | ||
public DataSource findSource(URI uri, FileType type) { | ||
if(skipUri(uri)) { return null; } | ||
BlobId blobId = GsHelper.toBlobId(uri); | ||
return createSource(blobId, type); | ||
} | ||
|
||
@Override | ||
public CompositeDataSource findCompositeSource(URI uri, FileType type) { | ||
if(skipUri(uri)) { return null; } | ||
return createCompositeSource(GsHelper.toBlobId(uri), type); | ||
} | ||
|
||
/* private methods */ | ||
|
||
private static boolean skipUri(URI uri) { | ||
return !"gs".equals(uri.getScheme()); | ||
} | ||
|
||
private DataSource createSource(BlobId blobId, FileType type) { | ||
Blob blob = storage.get(blobId); | ||
|
||
if(blob != null) { | ||
return new GsFileDataSource(blob, type); | ||
} | ||
else { | ||
return new GsOutFileDataSource(storage, blobId, type); | ||
} | ||
} | ||
|
||
private CompositeDataSource createCompositeSource(BlobId blobId, FileType type) { | ||
if(GsHelper.isRoot(blobId)) { | ||
return new GsDirectoryDataSource(storage, blobId, type); | ||
} | ||
|
||
if(blobId.getName().endsWith(".zip")) { | ||
Blob blob = storage.get(blobId); | ||
|
||
if(blob == null) { | ||
throw new IllegalArgumentException( | ||
type.text() + " not found: " + GsHelper.toUriString(blobId) | ||
); | ||
} | ||
DataSource gsSource = new GsFileDataSource(blob, type); | ||
return new ZipStreamDataSourceDecorator(gsSource); | ||
} | ||
return new GsDirectoryDataSource(storage, blobId, type); | ||
} | ||
|
||
private Storage connectToStorage() { | ||
try { | ||
StorageOptions.Builder builder = StorageOptions.getDefaultInstance().toBuilder(); | ||
|
||
if(credentialsFilename != null) { | ||
GoogleCredentials credentials = GoogleCredentials | ||
.fromStream(new FileInputStream(credentialsFilename)) | ||
.createScoped(Collections.singletonList( | ||
"https://www.googleapis.com/auth/cloud-platform")); | ||
builder.setCredentials(credentials); | ||
} | ||
return builder.build().getService(); | ||
} | ||
catch (IOException e) { | ||
throw new RuntimeException(e.getLocalizedMessage(), e); | ||
} | ||
} | ||
} |
104 changes: 104 additions & 0 deletions
104
src/ext/java/org/opentripplanner/ext/datastore/gs/GsDirectoryDataSource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
package org.opentripplanner.ext.datastore.gs; | ||
|
||
import com.google.api.gax.paging.Page; | ||
import com.google.cloud.storage.Blob; | ||
import com.google.cloud.storage.BlobId; | ||
import com.google.cloud.storage.Bucket; | ||
import com.google.cloud.storage.Storage; | ||
import org.opentripplanner.datastore.CompositeDataSource; | ||
import org.opentripplanner.datastore.DataSource; | ||
import org.opentripplanner.datastore.FileType; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Collection; | ||
import java.util.function.Consumer; | ||
|
||
|
||
/** | ||
* This is a an adapter to to simulate a file directory on a GCS. Files created using an instance of this | ||
* class wil have a common namespace. It does only support creating new output sources, it can not | ||
* be used to list files with the common namespace (directory path). | ||
*/ | ||
public class GsDirectoryDataSource extends AbstractGsDataSource implements CompositeDataSource { | ||
|
||
private final Storage storage; | ||
|
||
GsDirectoryDataSource(Storage storage, BlobId blobId, FileType type) { | ||
super(blobId, type); | ||
this.storage = storage; | ||
} | ||
|
||
@Override | ||
public boolean exists() { | ||
return getBucket().list( | ||
Storage.BlobListOption.prefix(name()), | ||
Storage.BlobListOption.pageSize(1), | ||
Storage.BlobListOption.currentDirectory() | ||
).getValues().iterator().hasNext(); | ||
} | ||
|
||
@Override | ||
public DataSource entry(String name) { | ||
Blob blob = childBlob(name); | ||
// If file exist | ||
if(blob != null) { | ||
return new GsFileDataSource(blob, type()); | ||
} | ||
// New file | ||
BlobId childBlobId = BlobId.of(bucketName(), childPath(name)); | ||
return new GsOutFileDataSource(storage, childBlobId, type()); | ||
} | ||
|
||
@Override | ||
public Collection<DataSource> content() { | ||
Collection<DataSource> content = new ArrayList<>(); | ||
forEachChildBlob(blob -> content.add(new GsFileDataSource(blob, type()))); | ||
return content; | ||
} | ||
|
||
@Override | ||
public void delete() { | ||
forEachChildBlob(Blob::delete); | ||
} | ||
|
||
@Override | ||
public void close() { } | ||
|
||
|
||
/* private methods */ | ||
|
||
private Bucket getBucket() { | ||
Bucket bucket = storage.get(bucketName()); | ||
if(bucket == null) { | ||
throw new IllegalArgumentException("Bucket not found: " + bucketName()); | ||
} | ||
return bucket; | ||
} | ||
|
||
private Blob childBlob(String name) { | ||
return getBucket().get(childPath(name)); | ||
} | ||
|
||
private String childPrefix() { | ||
return GsHelper.isRoot(blobId()) ? "" : name() + "/"; | ||
} | ||
|
||
private String childPath(String name) { | ||
return childPrefix() + name; | ||
} | ||
|
||
private void forEachChildBlob(Consumer<Blob> consumer) { | ||
int pathIndex = childPrefix().length(); | ||
for (Blob blob : listBlobs().iterateAll()) { | ||
String name = blob.getName().substring(pathIndex); | ||
// Skip nested content | ||
if(!name.contains("/")) { | ||
consumer.accept(blob); | ||
} | ||
} | ||
} | ||
|
||
private Page<Blob> listBlobs() { | ||
return getBucket().list(Storage.BlobListOption.prefix(childPrefix())); | ||
} | ||
} |
79 changes: 79 additions & 0 deletions
79
src/ext/java/org/opentripplanner/ext/datastore/gs/GsFileDataSource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package org.opentripplanner.ext.datastore.gs; | ||
|
||
import com.google.cloud.storage.Blob; | ||
import org.opentripplanner.datastore.DataSource; | ||
import org.opentripplanner.datastore.FileType; | ||
import org.opentripplanner.datastore.file.DirectoryDataSource; | ||
import org.opentripplanner.datastore.file.ZipFileDataSource; | ||
|
||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.io.OutputStream; | ||
import java.util.zip.GZIPInputStream; | ||
|
||
import static java.nio.channels.Channels.newInputStream; | ||
import static java.nio.channels.Channels.newOutputStream; | ||
|
||
/** | ||
* This class is a wrapper around and EXISTING Google Cloud Store bucket blob. It can | ||
* be read and overwritten. | ||
* <p> | ||
* Reading compressed blobs is supported. The only format supported is gzip (extension .gz). | ||
*/ | ||
class GsFileDataSource extends AbstractGsDataSource implements DataSource { | ||
private final Blob blob; | ||
|
||
|
||
/** | ||
* Create a data source wrapper around a file. This wrapper handles GZIP(.gz) compressed files | ||
* as well as normal files. It does not handle directories({@link DirectoryDataSource}) or | ||
* zip-files {@link ZipFileDataSource} witch contain multiple files. | ||
*/ | ||
GsFileDataSource(Blob blob, FileType type) { | ||
super(blob.getBlobId(), type); | ||
this.blob = blob; | ||
} | ||
|
||
@Override | ||
public long size() { | ||
return blob.getSize(); | ||
} | ||
|
||
@Override | ||
public long lastModified() { | ||
return blob.getUpdateTime(); | ||
} | ||
|
||
@Override | ||
public boolean exists() { | ||
return blob.exists(); | ||
} | ||
|
||
@Override | ||
public boolean isWritable() { | ||
return true; | ||
} | ||
|
||
@Override | ||
public InputStream asInputStream() { | ||
// We support both gzip and unzipped files when reading. | ||
InputStream in = newInputStream(blob.reader()); | ||
|
||
if (blob.getName().endsWith(".gz")) { | ||
try { | ||
return new GZIPInputStream(in); | ||
} | ||
catch (IOException e) { | ||
throw new IllegalStateException(e.getLocalizedMessage(), e); | ||
} | ||
} | ||
else { | ||
return in; | ||
} | ||
} | ||
|
||
@Override | ||
public OutputStream asOutputStream() { | ||
return newOutputStream(blob.writer()); | ||
} | ||
} |
Oops, something went wrong.