/* * Copyright 2015 Bounce Storage, Inc. <info@bouncestorage.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.bouncestorage.swiftproxy.v1; import static java.util.Objects.requireNonNull; import static com.google.common.base.Throwables.propagate; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import javax.inject.Singleton; import javax.validation.constraints.NotNull; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.HEAD; import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlType; import com.bouncestorage.swiftproxy.BlobStoreResource; import com.bouncestorage.swiftproxy.BounceResourceConfig; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.net.PercentEscaper; import org.glassfish.grizzly.http.server.Request; import org.jclouds.blobstore.BlobStore; import org.jclouds.blobstore.ContainerNotFoundException; import org.jclouds.blobstore.domain.StorageMetadata; @Singleton @Path("/v1/{account}") public final class AccountResource extends BlobStoreResource { private static final PercentEscaper BULK_DELETE_ESCAPER = new PercentEscaper("-_.~:/", false); @GET public Response getAccount(@NotNull @PathParam("account") String account, @QueryParam("limit") Optional<Integer> limit, @QueryParam("marker") Optional<String> marker, @QueryParam("end_marker") Optional<String> endMarker, @QueryParam("format") Optional<String> format, @QueryParam("prefix") Optional<String> prefix, @QueryParam("delimiter") Optional<String> delimiter, @HeaderParam("X-Auth-Token") String authToken, @HeaderParam("X-Newest") @DefaultValue("false") boolean newest, @HeaderParam("Accept") Optional<String> accept) { delimiter.ifPresent(x -> logger.info("delimiter not supported yet")); BlobStore blobStore = getBlobStore(authToken).get(); ArrayList<ContainerEntry> entries = blobStore.list() .stream() .map(StorageMetadata::getName) .filter(name -> marker.map(m -> name.compareTo(m) > 0).orElse(true)) .filter(name -> endMarker.map(m -> name.compareTo(m) < 0).orElse(true)) .filter(name -> prefix.map(name::startsWith).orElse(true)) .map(ContainerEntry::new) .collect(Collectors.toCollection(ArrayList::new)); MediaType formatType; if (format.isPresent()) { formatType = BounceResourceConfig.getMediaType(format.get()); } else if (accept.isPresent()) { formatType = MediaType.valueOf(accept.get()); } else { formatType = MediaType.TEXT_PLAIN_TYPE; } if (blobStore.getContext().unwrap().getId().equals("transient")) { entries.sort((a, b) -> a.getName().compareTo(b.getName())); } long count = entries.size(); limit.ifPresent((max) -> { if (entries.size() > max) { entries.subList(max, entries.size()).clear(); } }); Account root = new Account(); root.name = account; root.container = entries; return output(root, entries, formatType) .header("X-Account-Container-Count", count) .header("X-Account-Object-Count", -1) .header("X-Account-Bytes-Used", -1) .header("X-Timestamp", -1) .header("X-Trans-Id", -1) .header("Accept-Ranges", "bytes") .build(); } @HEAD public Response headAccount(@NotNull @PathParam("account") String account, @HeaderParam("X-Auth-Token") String authToken, @HeaderParam("X-Newest") boolean newest) { return Response.noContent() .header("X-Account-Container-Count", -1) .header("X-Account-Object-Count", -1) .header("X-Account-Bytes-Used", -1) .header("X-Timestamp", -1) .header("X-Trans-Id", -1) .header("Accept-Ranges", "bytes") .build(); } @DELETE @Consumes(MediaType.TEXT_PLAIN) @Produces(MediaType.APPLICATION_JSON) public BulkDeleteResult bulkDeleteDelete(@NotNull @PathParam("account") String account, @QueryParam("bulk-delete") String bulkDelete, @HeaderParam("X-Auth-Token") String authToken, @Context Request request) throws JsonProcessingException { return bulkDelete(account, bulkDelete, authToken, request); } @POST @Consumes(MediaType.TEXT_PLAIN) @Produces(MediaType.APPLICATION_JSON) public BulkDeleteResult bulkDelete(@NotNull @PathParam("account") String account, @QueryParam("bulk-delete") String bulkDelete, @HeaderParam("X-Auth-Token") String authToken, @Context Request request) throws JsonProcessingException { if (bulkDelete == null) { // TODO: Currently this will match the account delete request as well, which we do not implement throw new WebApplicationException(Response.Status.NOT_IMPLEMENTED); } BlobStore blobStore = getBlobStore(authToken).get(); if (blobStore == null) { throw new WebApplicationException(Response.Status.BAD_REQUEST); } String line; ArrayList<String> objects = new ArrayList<>(); boolean isTransient = blobStore.getContext().unwrap().getId().equals("transient"); try (BufferedReader in = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8))) { while ((line = in.readLine()) != null) { if (isTransient) { // jclouds does not escape things correctly line = BULK_DELETE_ESCAPER.escape(URLDecoder.decode(line, "UTF-8")); } objects.add(line); } } catch (IOException e) { throw propagate(e); } BulkDeleteResult result = new BulkDeleteResult(); for (String objectContainer : objects) { try { if (objectContainer.startsWith("/")) { objectContainer = objectContainer.substring(1); } int separatorIndex = objectContainer.indexOf('/'); if (separatorIndex < 0) { blobStore.deleteContainer(objectContainer.substring(1)); result.numberDeleted += 1; continue; } String container = objectContainer.substring(0, separatorIndex); String object = objectContainer.substring(separatorIndex + 1); if (!blobStore.blobExists(container, object)) { result.numberNotFound += 1; } else { blobStore.removeBlob(container, object); result.numberDeleted += 1; } } catch (ContainerNotFoundException e) { result.numberNotFound += 1; } catch (Exception e) { e.printStackTrace(); result.errors.add(objectContainer); } } if (result.errors.isEmpty()) { result.responseStatus = Response.Status.OK.toString(); return result; } else { ObjectMapper mapper = new ObjectMapper(); result.responseStatus = Response.Status.BAD_GATEWAY.toString(); throw new WebApplicationException(mapper.writeValueAsString(result), Response.Status.BAD_GATEWAY); } } @XmlRootElement(name = "account") @XmlType static class Account { @XmlElement List<ContainerEntry> container; @XmlAttribute private String name; } @XmlRootElement(name = "container") @XmlType static class ContainerEntry { @XmlElement private String name; @XmlElement private long count; @XmlElement private long bytes; // for jackson XML public ContainerEntry() { } @JsonCreator public ContainerEntry(@JsonProperty("name") String name) { this.name = requireNonNull(name); } @Override public boolean equals(Object other) { return other instanceof ContainerEntry && name.equals(((ContainerEntry) other).name); } @Override public int hashCode() { return Objects.hash(name, count, bytes); } public String getName() { return name; } @Override public String toString() { return name; } } static class BulkDeleteResult { @JsonProperty("Response Status") String responseStatus; @JsonProperty("Errors") ArrayList<String> errors; @JsonProperty("Number Deleted") int numberDeleted; @JsonProperty("Number Not Found") int numberNotFound; BulkDeleteResult() { errors = new ArrayList<>(); numberDeleted = 0; numberNotFound = 0; } } }