/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.ambari.view.commons.hdfs;
import org.apache.ambari.view.ViewContext;
import org.apache.ambari.view.commons.exceptions.NotFoundFormattedException;
import org.apache.ambari.view.commons.exceptions.ServiceFormattedException;
import org.apache.ambari.view.utils.hdfs.HdfsApi;
import org.apache.ambari.view.utils.hdfs.HdfsApiException;
import org.json.simple.JSONObject;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
/**
* File operations service
*/
public class FileOperationService extends HdfsService {
/**
* Constructor
* @param context View Context instance
*/
public FileOperationService(ViewContext context) {
super(context);
}
/**
* Constructor
* @param context View Context instance
*/
public FileOperationService(ViewContext context, Map<String, String> customProperties) {
super(context, customProperties);
}
/**
* List dir
* @param path path
* @return response with dir content
*/
@GET
@Path("/listdir")
@Produces(MediaType.APPLICATION_JSON)
public Response listdir(@QueryParam("path") String path) {
try {
JSONObject response = new JSONObject();
response.put("files", getApi().fileStatusToJSON(getApi().listdir(path)));
response.put("meta", getApi().fileStatusToJSON(getApi().getFileStatus(path)));
return Response.ok(response).build();
} catch (WebApplicationException ex) {
throw ex;
} catch (FileNotFoundException ex) {
throw new NotFoundFormattedException(ex.getMessage(), ex);
} catch (Exception ex) {
throw new ServiceFormattedException(ex.getMessage(), ex);
}
}
/**
* Rename
* @param request rename request
* @return response with success
*/
@POST
@Path("/rename")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response rename(final SrcDstFileRequest request) {
try {
HdfsApi api = getApi();
ResponseBuilder result;
if (api.rename(request.src, request.dst)) {
result = Response.ok(getApi().fileStatusToJSON(api
.getFileStatus(request.dst)));
} else {
result = Response.ok(new FileOperationResult(false, "Can't move '" + request.src + "' to '" + request.dst + "'")).status(422);
}
return result.build();
} catch (WebApplicationException ex) {
throw ex;
} catch (Exception ex) {
throw new ServiceFormattedException(ex.getMessage(), ex);
}
}
/**
* Chmod
* @param request chmod request
* @return response with success
*/
@POST
@Path("/chmod")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response chmod(final ChmodRequest request) {
try {
HdfsApi api = getApi();
ResponseBuilder result;
if (api.chmod(request.path, request.mode)) {
result = Response.ok(getApi().fileStatusToJSON(api
.getFileStatus(request.path)));
} else {
result = Response.ok(new FileOperationResult(false, "Can't chmod '" + request.path + "'")).status(422);
}
return result.build();
} catch (WebApplicationException ex) {
throw ex;
} catch (Exception ex) {
throw new ServiceFormattedException(ex.getMessage(), ex);
}
}
/**
* Copy file
* @param request source and destination request
* @return response with success
*/
@POST
@Path("/move")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response move(final MultiSrcDstFileRequest request,
@Context HttpHeaders headers, @Context UriInfo ui) {
try {
HdfsApi api = getApi();
ResponseBuilder result;
String message = "";
List<String> sources = request.sourcePaths;
String destination = request.destinationPath;
if(sources.isEmpty()) {
result = Response.ok(new FileOperationResult(false, "Can't move 0 file/folder to '" + destination + "'")).
status(422);
return result.build();
}
int index = 0;
for (String src : sources) {
String fileName = getFileName(src);
String finalDestination = getDestination(destination, fileName);
try {
if (api.rename(src, finalDestination)) {
index ++;
} else {
message = "Failed to move '" + src + "' to '" + finalDestination + "'";
break;
}
} catch (IOException exception) {
message = exception.getMessage();
logger.error("Failed to move '{}' to '{}'. Exception: {}", src, finalDestination,
exception.getMessage());
break;
}
}
if (index == sources.size()) {
result = Response.ok(new FileOperationResult(true)).status(200);
} else {
FileOperationResult errorResult = getFailureFileOperationResult(sources, index, message);
result = Response.ok(errorResult).status(422);
}
return result.build();
} catch (Exception ex) {
throw new ServiceFormattedException(ex.getMessage(), ex);
}
}
/**
* Copy file
* @param request source and destination request
* @return response with success
*/
@POST
@Path("/copy")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response copy(final MultiSrcDstFileRequest request,
@Context HttpHeaders headers, @Context UriInfo ui) {
try {
HdfsApi api = getApi();
ResponseBuilder result;
String message = "";
List<String> sources = request.sourcePaths;
String destination = request.destinationPath;
if(sources.isEmpty()) {
result = Response.ok(new FileOperationResult(false, "Can't copy 0 file/folder to '" + destination + "'")).
status(422);
return result.build();
}
int index = 0;
for (String src : sources) {
String fileName = getFileName(src);
String finalDestination = getDestination(destination, fileName);
try {
api.copy(src, finalDestination);
index ++;
} catch (IOException|HdfsApiException exception) {
message = exception.getMessage();
logger.error("Failed to copy '{}' to '{}'. Exception: {}", src, finalDestination,
exception.getMessage());
break;
}
}
if (index == sources.size()) {
result = Response.ok(new FileOperationResult(true)).status(200);
} else {
FileOperationResult errorResult = getFailureFileOperationResult(sources, index, message);
result = Response.ok(errorResult).status(422);
}
return result.build();
} catch (Exception ex) {
throw new ServiceFormattedException(ex.getMessage(), ex);
}
}
/**
* Make directory
* @param request make directory request
* @return response with success
*/
@PUT
@Path("/mkdir")
@Produces(MediaType.APPLICATION_JSON)
public Response mkdir(final MkdirRequest request) {
try{
HdfsApi api = getApi();
ResponseBuilder result;
if (api.mkdir(request.path)) {
result = Response.ok(getApi().fileStatusToJSON(api.getFileStatus(request.path)));
} else {
result = Response.ok(new FileOperationResult(false, "Can't create dir '" + request.path + "'")).status(422);
}
return result.build();
} catch (WebApplicationException ex) {
throw ex;
} catch (Exception ex) {
throw new ServiceFormattedException(ex.getMessage(), ex);
}
}
/**
* Empty trash
* @return response with success
*/
@DELETE
@Path("/trash/emptyTrash")
@Produces(MediaType.APPLICATION_JSON)
public Response emptyTrash() {
try {
HdfsApi api = getApi();
api.emptyTrash();
return Response.ok(new FileOperationResult(true)).build();
} catch (WebApplicationException ex) {
throw ex;
} catch (Exception ex) {
throw new ServiceFormattedException(ex.getMessage(), ex);
}
}
/**
* Move to trash
* @param request remove request
* @return response with success
*/
@DELETE
@Path("/moveToTrash")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response moveToTrash(MultiRemoveRequest request) {
try {
ResponseBuilder result;
HdfsApi api = getApi();
String trash = api.getTrashDirPath();
String message = "";
if (request.paths.size() == 0) {
result = Response.ok(new FileOperationResult(false, "No path entries provided.")).status(422);
} else {
if (!api.exists(trash)) {
if (!api.mkdir(trash)) {
result = Response.ok(new FileOperationResult(false, "Trash dir does not exists. Can't create dir for " +
"trash '" + trash + "'")).status(422);
return result.build();
}
}
int index = 0;
for (MultiRemoveRequest.PathEntry entry : request.paths) {
String trashFilePath = api.getTrashDirPath(entry.path);
try {
if (api.rename(entry.path, trashFilePath)) {
index ++;
} else {
message = "Failed to move '" + entry.path + "' to '" + trashFilePath + "'";
break;
}
} catch (IOException exception) {
message = exception.getMessage();
logger.error("Failed to move '{}' to '{}'. Exception: {}", entry.path, trashFilePath,
exception.getMessage());
break;
}
}
if (index == request.paths.size()) {
result = Response.ok(new FileOperationResult(true)).status(200);
} else {
FileOperationResult errorResult = getFailureFileOperationResult(getPathsFromPathsEntries(request.paths), index, message);
result = Response.ok(errorResult).status(422);
}
}
return result.build();
} catch (Exception ex) {
throw new ServiceFormattedException(ex.getMessage(), ex);
}
}
/**
* Remove
* @param request remove request
* @return response with success
*/
@DELETE
@Path("/remove")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response remove(MultiRemoveRequest request, @Context HttpHeaders headers,
@Context UriInfo ui) {
try {
HdfsApi api = getApi();
ResponseBuilder result;
String message = "";
if(request.paths.size() == 0) {
result = Response.ok(new FileOperationResult(false, "No path entries provided."));
} else {
int index = 0;
for (MultiRemoveRequest.PathEntry entry : request.paths) {
try {
if (api.delete(entry.path, entry.recursive)) {
index++;
} else {
message = "Failed to remove '" + entry.path + "'";
break;
}
} catch (IOException exception) {
message = exception.getMessage();
logger.error("Failed to remove '{}'. Exception: {}", entry.path, exception.getMessage());
break;
}
}
if (index == request.paths.size()) {
result = Response.ok(new FileOperationResult(true)).status(200);
} else {
FileOperationResult errorResult = getFailureFileOperationResult(getPathsFromPathsEntries(request.paths), index, message);
result = Response.ok(errorResult).status(422);
}
}
return result.build();
} catch (Exception ex) {
throw new ServiceFormattedException(ex.getMessage(), ex);
}
}
private List<String> getPathsFromPathsEntries(List<MultiRemoveRequest.PathEntry> paths) {
List<String> entries = new ArrayList<>();
for(MultiRemoveRequest.PathEntry path: paths) {
entries.add(path.path);
}
return entries;
}
private FileOperationResult getFailureFileOperationResult(List<String> paths, int failedIndex, String message) {
List<String> succeeded = new ArrayList<>();
List<String> unprocessed = new ArrayList<>();
List<String> failed = new ArrayList<>();
ListIterator<String> iter = paths.listIterator();
while (iter.hasNext()) {
int index = iter.nextIndex();
String path = iter.next();
if (index < failedIndex) {
succeeded.add(path);
} else if (index == failedIndex) {
failed.add(path);
} else {
unprocessed.add(path);
}
}
return new FileOperationResult(false, message, succeeded, failed, unprocessed);
}
private String getDestination(String baseDestination, String fileName) {
if(baseDestination.endsWith("/")) {
return baseDestination + fileName;
} else {
return baseDestination + "/" + fileName;
}
}
private String getFileName(String srcPath) {
return srcPath.substring(srcPath.lastIndexOf('/') + 1);
}
/**
* Wrapper for json mapping of mkdir request
*/
@XmlRootElement
public static class MkdirRequest {
@XmlElement(nillable = false, required = true)
public String path;
}
/**
* Wrapper for json mapping of chmod request
*/
@XmlRootElement
public static class ChmodRequest {
@XmlElement(nillable = false, required = true)
public String path;
@XmlElement(nillable = false, required = true)
public String mode;
}
/**
* Wrapper for json mapping of request with
* source and destination
*/
@XmlRootElement
public static class SrcDstFileRequest {
@XmlElement(nillable = false, required = true)
public String src;
@XmlElement(nillable = false, required = true)
public String dst;
}
/**
* Wrapper for json mapping of request with multiple
* source and destination
*/
@XmlRootElement
public static class MultiSrcDstFileRequest {
@XmlElement(nillable = false, required = true)
public List<String> sourcePaths = new ArrayList<>();
@XmlElement(nillable = false, required = true)
public String destinationPath;
}
/**
* Wrapper for json mapping of remove request
*/
@XmlRootElement
public static class MultiRemoveRequest {
@XmlElement(nillable = false, required = true)
public List<PathEntry> paths = new ArrayList<>();
public static class PathEntry {
@XmlElement(nillable = false, required = true)
public String path;
public boolean recursive;
}
}
}