This repository has been archived by the owner on Sep 2, 2022. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(files): add a tab on the person page to create, list, download a…
…nd delete files (documents) for that person The files are stored in Google Cloud Storage. See the README for details.
- Loading branch information
Showing
33 changed files
with
1,314 additions
and
28 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 |
---|---|---|
|
@@ -26,4 +26,5 @@ nbdist/ | |
.nb-gradle/ | ||
|
||
classes | ||
.java-version | ||
.java-version | ||
secrets |
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
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
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,62 @@ | ||
package org.globe42.storage; | ||
|
||
import java.time.Instant; | ||
|
||
import com.google.cloud.storage.BlobInfo; | ||
|
||
/** | ||
* Information about a file (stored in a Google Cloud Storage) | ||
* @author JB Nizet | ||
*/ | ||
public final class FileDTO { | ||
|
||
/** | ||
* The name of the file | ||
*/ | ||
private final String name; | ||
|
||
/** | ||
* The size of the file, in bytes | ||
*/ | ||
private final Long size; | ||
|
||
/** | ||
* The instant when the file was created in the storage | ||
*/ | ||
private final Instant creationInstant; | ||
|
||
/** | ||
* The content type of the file | ||
*/ | ||
private final String contentType; | ||
|
||
public FileDTO(BlobInfo blob, String prefix) { | ||
this.name = blob.getName().substring(prefix.length()); | ||
this.size = blob.getSize(); | ||
this.creationInstant = blob.getCreateTime() == null ? Instant.now() : Instant.ofEpochMilli(blob.getCreateTime()); | ||
this.contentType = blob.getContentType(); | ||
} | ||
|
||
public FileDTO(String name, Long size, Instant creationInstant, String contentType) { | ||
this.name = name; | ||
this.size = size; | ||
this.creationInstant = creationInstant; | ||
this.contentType = contentType; | ||
} | ||
|
||
public String getName() { | ||
return name; | ||
} | ||
|
||
public Long getSize() { | ||
return size; | ||
} | ||
|
||
public Instant getCreationInstant() { | ||
return creationInstant; | ||
} | ||
|
||
public String getContentType() { | ||
return contentType; | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
backend/src/main/java/org/globe42/storage/ReadableFile.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,28 @@ | ||
package org.globe42.storage; | ||
|
||
import java.io.InputStream; | ||
import java.nio.channels.Channels; | ||
|
||
import com.google.cloud.storage.Blob; | ||
|
||
/** | ||
* A file that can be read | ||
* @author JB Nizet | ||
*/ | ||
public class ReadableFile { | ||
private final FileDTO file; | ||
private final Blob blob; | ||
|
||
public ReadableFile(Blob blob, String prefix) { | ||
this.blob = blob; | ||
this.file = new FileDTO(blob, prefix); | ||
} | ||
|
||
public InputStream getInputStream() { | ||
return Channels.newInputStream(this.blob.reader()); | ||
} | ||
|
||
public FileDTO getFile() { | ||
return file; | ||
} | ||
} |
78 changes: 78 additions & 0 deletions
78
backend/src/main/java/org/globe42/storage/StorageConfig.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,78 @@ | ||
package org.globe42.storage; | ||
|
||
import java.io.ByteArrayInputStream; | ||
import java.io.File; | ||
import java.io.FileInputStream; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.nio.charset.StandardCharsets; | ||
|
||
import com.google.auth.oauth2.GoogleCredentials; | ||
import com.google.cloud.storage.Storage; | ||
import com.google.cloud.storage.StorageOptions; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
|
||
/** | ||
* Config class for Google Cloud Storage beans. See the README for useful links regarding Google Cloud Storage. | ||
* @author JB Nizet | ||
*/ | ||
@Configuration | ||
public class StorageConfig { | ||
|
||
private static final Logger LOGGER = LoggerFactory.getLogger(StorageConfig.class); | ||
|
||
/** | ||
* The JSON string containing the credentials, loaded from the property | ||
* <code>globe42.googleCloudStorageCredentials</code>. If not | ||
* null (typically in production, on clever cloud, where the whole JSON credentials are stored in an environment | ||
* variable) then this is used as the source of the google credentials. | ||
*/ | ||
private final String credentials; | ||
|
||
/** | ||
* The path to the json file containing the credentials, loaded from the property | ||
* <code>globe42.googleCloudStorageCredentialsPath</code>. Only used if {@link #credentials} is null. | ||
* Typically used in dev mode, where specifying a file path in a command-line property is easier. | ||
*/ | ||
private final File credentialsPath; | ||
|
||
public StorageConfig(@Value("${globe42.googleCloudStorageCredentials:#{null}}") String credentials, | ||
@Value("${globe42.googleCloudStorageCredentialsPath:#{null}}") File credentialsPath) { | ||
this.credentials = credentials; | ||
this.credentialsPath = credentialsPath; | ||
} | ||
|
||
@Bean | ||
public Storage storage() throws IOException { | ||
if (this.credentials != null) { | ||
LOGGER.info("Property globe42.googleCloudStorageCredentials is set." + | ||
" Using its value as Google Cloud Storage JSON credentials"); | ||
InputStream in = new ByteArrayInputStream(this.credentials.getBytes(StandardCharsets.UTF_8)); | ||
return StorageOptions | ||
.newBuilder() | ||
.setCredentials(GoogleCredentials.fromStream(in)) | ||
.build() | ||
.getService(); | ||
} | ||
else if (this.credentialsPath != null) { | ||
LOGGER.info("Property globe42.googleCloudStorageCredentialsPath is set." + | ||
" Using its value as a JSON file path to the Google Cloud Storage credentials"); | ||
try (InputStream in = new FileInputStream(this.credentialsPath)) { | ||
return StorageOptions | ||
.newBuilder() | ||
.setCredentials(GoogleCredentials.fromStream(in)) | ||
.build() | ||
.getService(); | ||
} | ||
} | ||
else { | ||
LOGGER.warn("Neither property globe42.googleCloudStorageCredentials nor globe42.googleCloudStorageCredentials is set." + | ||
" Using default instance credentials."); | ||
return StorageOptions.getDefaultInstance().getService(); | ||
} | ||
} | ||
} |
93 changes: 93 additions & 0 deletions
93
backend/src/main/java/org/globe42/storage/StorageService.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,93 @@ | ||
package org.globe42.storage; | ||
|
||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.io.OutputStream; | ||
import java.io.UncheckedIOException; | ||
import java.nio.channels.Channels; | ||
import java.util.List; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.StreamSupport; | ||
|
||
import com.google.api.gax.paging.Page; | ||
import com.google.cloud.storage.Blob; | ||
import com.google.cloud.storage.BlobId; | ||
import com.google.cloud.storage.BlobInfo; | ||
import com.google.cloud.storage.Storage; | ||
import com.google.common.io.ByteStreams; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.stereotype.Service; | ||
|
||
/** | ||
* Service used to wrap the Google cloud storage API | ||
* @author JB Nizet | ||
*/ | ||
@Service | ||
public class StorageService { | ||
|
||
static final String PERSON_FILES_BUCKET = "personfiles"; | ||
|
||
private final Storage storage; | ||
|
||
public StorageService(Storage storage) { | ||
this.storage = storage; | ||
} | ||
|
||
/** | ||
* Lists the files in the given directory | ||
* @param directory the directory (serving as a prefix) to list the files | ||
* @return the list of files in the given directory. The directory prefix is stripped of from the names of the | ||
* returned files | ||
*/ | ||
public List<FileDTO> list(String directory) { | ||
final String prefix = toPrefix(directory); | ||
Page<Blob> page = storage.list(PERSON_FILES_BUCKET, | ||
Storage.BlobListOption.pageSize(10_000), | ||
Storage.BlobListOption.currentDirectory(), | ||
Storage.BlobListOption.prefix(prefix)); | ||
return StreamSupport.stream(page.getValues().spliterator(), false) | ||
.map(blob -> new FileDTO(blob, prefix)) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
/** | ||
* Gets the given file in the given directory | ||
* @param directory the directory (serving as a prefix) of the file | ||
* @return the file in the given directory. The directory prefix is stripped of from the name of the | ||
* returned file | ||
*/ | ||
public ReadableFile get(String directory, String name) { | ||
String prefix = toPrefix(directory); | ||
Blob blob = storage.get(PERSON_FILES_BUCKET, prefix + name); | ||
return new ReadableFile(blob, prefix); | ||
} | ||
|
||
public FileDTO create(String directory, | ||
String name, | ||
String contentType, | ||
InputStream data) { | ||
String prefix = toPrefix(directory); | ||
BlobId blobId = BlobId.of(PERSON_FILES_BUCKET, prefix + name); | ||
if (contentType == null) { | ||
contentType = MediaType.APPLICATION_OCTET_STREAM.toString(); | ||
} | ||
BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setContentType(contentType).build(); | ||
try (OutputStream out = Channels.newOutputStream(storage.writer(blobInfo))) { | ||
ByteStreams.copy(data, out); | ||
} | ||
catch (IOException e) { | ||
throw new UncheckedIOException(e); | ||
} | ||
|
||
return new FileDTO(blobInfo, prefix); | ||
} | ||
|
||
public void delete(String directory, String name) { | ||
String prefix = toPrefix(directory); | ||
storage.delete(PERSON_FILES_BUCKET, prefix + name); | ||
} | ||
|
||
private String toPrefix(String directory) { | ||
return directory.endsWith("/") ? directory : directory + "/"; | ||
} | ||
} |
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,23 @@ | ||
package org.globe42.web; | ||
|
||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; | ||
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer; | ||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||
|
||
/** | ||
* Configuration class used to configure a thread pool used for asynchronous request processing (file downloads) | ||
* @author JB Nizet | ||
*/ | ||
@Configuration | ||
public class AsyncConfig implements WebMvcConfigurer { | ||
@Override | ||
public void configureAsyncSupport(AsyncSupportConfigurer configurer) { | ||
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); | ||
taskExecutor.setCorePoolSize(1); | ||
taskExecutor.setMaxPoolSize(30); | ||
taskExecutor.setThreadNamePrefix("GlobeWebAsync"); | ||
taskExecutor.initialize(); | ||
configurer.setTaskExecutor(taskExecutor); | ||
} | ||
} |
Oops, something went wrong.