WebDAV

来自wiki
跳到导航 跳到搜索

基于Controller,可以实现WebDAV功能访问文件资源。

/*
 * Copyright 2015 JIHU, Inc. and/or its affiliates.
 *
*/
package org.giiwa.fcloud.web;

import java.io.IOException;
import java.util.Arrays;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.giiwa.core.bean.Beans;
import org.giiwa.core.bean.UID;
import org.giiwa.core.bean.X;
import org.giiwa.core.bean.Helper.W;
import org.giiwa.core.conf.Global;
import org.giiwa.core.json.JSON;
import org.giiwa.core.task.Task;
import org.giiwa.fcloud.bean.FFile;
import org.giiwa.fcloud.bean.Quato;
import org.giiwa.fcloud.bean.Share;
import org.giiwa.fcloud.fo.FO;
import org.giiwa.framework.bean.Code;
import org.giiwa.framework.bean.GLog;
import org.giiwa.framework.bean.User;
import org.giiwa.framework.web.Controller;
import org.giiwa.framework.web.Path;

/**
 * web api: /fcloud/*.
 *
 * @author joe
 */
public class fc extends Controller {

	@Override
	public void onGet() {
		String fullname = this.path;
		if (X.isEmpty(fullname))
			return;

		if (!fullname.startsWith("/")) {
			fullname = "/" + fullname;
		}

		User u = this.getUser();
		Quato a = u == null ? null : Quato.load(u.getId());

		if (!Share.isReadable(fullname, a)) {
			GLog.applog.warn(fc.class, "download", "access deny, fullname=" + fullname);
			return;
		}

		try {
			FO f = FO.seek(fullname);

			this.response(f.getName(), f.getInputStream(), f.length());

		} catch (Exception e) {
			log.error(e.getMessage(), e);
		}
	}

	public static void main(String[] args) {
		long a = Long.MAX_VALUE;

		System.out.println((double) a);
	}

	private String fullname(Code code, String path) throws IOException {

		String fullname = code.getString("fullname");
		if (X.isEmpty(fullname)) {
			throw new IOException("参数错误!");
		}
		fullname = X.getCanonicalPath(fullname + "/" + path);

		log.debug("fullname=" + fullname);

		return fullname;
	}

	private String shortname(Code code, String path) throws IOException {
		String fullname = code.getString("fullname");
		if (X.isEmpty(fullname)) {
			throw new IOException("参数错误!");
		}
		return path.replace(fullname, X.EMPTY);
	}

	@Path(path = "files/(.*)", method = "*")
	public void files(String path) {

		int i = path.indexOf("/");
		String code = i > 0 ? path.substring(0, i) : path;
		path = i > 0 ? path.substring(i + 1) : "/";
		i = code.indexOf("_");

		String username = i > 0 ? code.substring(0, i) : null;

		code = i > 0 ? code.substring(i + 1) : code;

		log.debug("method=" + method.name + ", code=" + code + ", path=" + path);

		try {
			if (method.is("OPTIONS")) {

				resp.addHeader("DAV", "1,2");
				resp.addHeader("Allow", determineMethodsAllowed(req));
				resp.addHeader("MS-Author-Via", "DAV");
				return;
			}

			Code c1 = Code.load("fcloud", code);
			if (c1 == null) {

				int len = req.getContentLength();
				if (len > 0) {
					byte[] bb = new byte[len];
					req.getInputStream().read(bb);
					JSON prop = JSON.fromXml(new String(bb));
					prop = (JSON) prop.get("propfind.prop");
					log.debug("req=" + prop.toPrettyString());
				}

				if (method.is("PROPFIND")) {
					// let's able to umount
					String xml = "<?xml version=\"1.0\" ?><d:multistatus xmlns:d=\"DAV:\">";
					xml += "<d:response><d:href>" + Global.getString("site.url", "http://127.0.0.1") + "/fc/files/"
							+ X.getCanonicalPath(username + "_" + code + "/" + path) + "</d:href>";

					xml += "<d:propstat>";
					xml += "<d:status>HTTP/1.1 404 Not Found</d:status>";
					xml += "</d:propstat>";

					xml += "</d:response>";
					xml += "</d:multistatus>";
					resp.getOutputStream().write(xml.toString().getBytes());

				} else {
					this.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
				}
				return;
			}

			log.debug("head:");
			log.debug(Arrays.toString(this.getHeaders()));

			if (method.is("PROPFIND")) {
				_propfind(c1, path);
			} else if (method.is("GET")) {
				_get(c1, path);
			} else if (method.is("DELETE")) {
				_delete(c1, path);
			} else if (method.is("MKCOL")) {
				_mkcol(c1, path);
			} else if (method.is("LOCK")) {
				_lock(c1, path);
			} else if (method.is("PUT")) {
				_put(c1, path);
			}

		} catch (Exception e) {
			log.error(e.getMessage(), e);
//			resp.sendError(HttpServletResponse.SC_EXPECTATION_FAILED);
		}
	}

	private void _put(Code code, String path) throws IOException {

		String filename = fullname(code, path);

		String range = this.getHeader("Content-Range");
		if (X.isEmpty(range)) {
			FO f1 = FO.seek(filename);
			if (f1.exists() && f1.isDirectory()) {
				resp.addHeader("Allow", determineMethodsAllowed(req));
				this.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
				return;
			}
			long len = f1.put(req.getInputStream(), 0);
			log.debug("put, filename=" + filename + ", len=" + len);
			if (len > 0) {
				this.setStatus(HttpServletResponse.SC_CREATED);
			} else {
				this.setStatus(HttpServletResponse.SC_NO_CONTENT);
			}
		} else {

		}

		// this.sendError(HttpServletResponse.SC_CREATED, "Created");
	}

	private void _lock(Code code, String path) throws IOException {

		int lockRequestType = LOCK_CREATION;

		JSON prop = null;
		int len = req.getContentLength();
		if (len > 0) {
			byte[] bb = new byte[len];
			req.getInputStream().read(bb);
			prop = JSON.fromXml(new String(bb));
//			prop = (JSON) prop.get("propfind.prop");
			log.debug("bb=" + prop.toPrettyString());
			if (prop == null || prop.get("lockinfo.owner.href.body") == null) {
				lockRequestType = LOCK_REFRESH;
			}
		} else {
			lockRequestType = LOCK_REFRESH;
		}

		String filename = fullname(code, path);
		FO f1 = FO.seek(filename);
		f1.put(null, 0);

		String lockToken = UID.id(filename, System.currentTimeMillis());

		String xml = "<?xml version=\"1.0\" ?><d:multistatus xmlns:d=\"DAV:\">";
		xml += "<d:response><d:href>" + Global.getString("site.url", "http://127.0.0.1") + "/fc/files/"
				+ X.getCanonicalPath(code.getString("s2") + "/" + path) + "</d:href>";

		xml += "<d:status>HTTP/1.1 423 Locked</d:status>";
		xml += "</d:multistatus>";
		resp.addHeader("Lock-Token", "<opaquelocktoken:" + lockToken + ">");
		resp.getOutputStream().write(xml.toString().getBytes());

//		String owner = prop == null ? X.EMPTY : prop.getString("lockinfo.owner.href.body");
//		f1.put(new ByteArrayInputStream(owner.getBytes()), 0);
//		this.sendError(423, "Locked");
	}

	private void _mkcol(Code code, String path) throws IOException {

		JSON prop = null;
		int len = req.getContentLength();
		if (len > 0) {
			byte[] bb = new byte[len];
			req.getInputStream().read(bb);
			prop = JSON.fromXml(new String(bb));
//			prop = (JSON) prop.get("propfind.prop");
			log.debug("bb=" + prop.toPrettyString());
		}

		FO f1 = FO.seek(fullname(code, path));
		if (f1.exists()) {
			resp.addHeader("Allow", determineMethodsAllowed(req));
			this.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
			return;
		}
		f1.mkdirs();

		this.setStatus(HttpServletResponse.SC_CREATED);
		// this.sendError(HttpServletResponse.SC_CREATED, "Created");
	}

	private void _delete(Code code, String path) throws IOException {

		String fullname = fullname(code, path);
		FO f1 = FO.seek(fullname);
		if (f1.exists()) {
			f1.delete();
		}

		FFile.dao.delete(W.create().and("_parent", fullname));

		this.setStatus(HttpServletResponse.SC_NO_CONTENT);

	}

	private void _get(Code code, String path) throws IOException {
		FO f = FO.seek(fullname(code, path));
		if (f.exists()) {
			this.setContentType(Controller.getMimeType(f.getName()));
			IOUtils.copy(f.getInputStream(), this.getOutputStream());
		} else {
			resp.addHeader("Allow", determineMethodsAllowed(req));
			this.sendError(HttpServletResponse.SC_NOT_FOUND);
		}
	}

	private void _propfind(Code code, String path) throws IOException {

		JSON prop = null;
		int len = req.getContentLength();
		if (len > 0) {
			byte[] bb = new byte[len];
			req.getInputStream().read(bb);
			prop = JSON.fromXml(new String(bb));
			prop = (JSON) prop.get("propfind.prop");
			log.debug("bb=" + prop.toPrettyString());
		}

		String xml = "<?xml version=\"1.0\" ?><d:multistatus xmlns:d=\"DAV:\">";

		String filename = fullname(code, path);
		FFile f = FFile.load(filename);
		if (f != null) {

//			log.debug("f=" + f.getJSON());

			if (f.isFile()) {
				xml += file2xml(code, f, prop);
			} else if (f.isDir()) {
				xml += _list(code, f, prop);
			}

			xml += "</d:multistatus>";

			log.debug(xml);

			resp.getOutputStream().write(xml.toString().getBytes());

		} else {

			log.debug("not found the path=" + path + ", fullname=" + filename);

//			xml += _notfound(code, path);
			resp.addHeader("Allow", determineMethodsAllowed(req));
			this.sendError(HttpServletResponse.SC_NOT_FOUND);

		}

	}

	private String _notfound(Code code, String path) {
		String xml = X.EMPTY;
		xml += "<d:response><d:href>" + Global.getString("site.url", "http://127.0.0.1") + "/fc/files/"
				+ X.getCanonicalPath(code.getString("s2") + "/" + path) + "</d:href>";

		xml += "<d:propstat>";
		xml += "<d:status>HTTP/1.1 404 Not Found</d:status>";
		xml += "</d:propstat>";

		xml += "</d:response>";
		return xml;
	}

	private String _list(Code code, FFile f, JSON prop) throws IOException {

		String xml = X.EMPTY;

		W q = W.create().and("_parent", f.getFullname()).sort("dir", -1).sort("name", 1);
		Beans<FFile> ff = FFile.dao.load(q, 0, 10000);

//		log.debug("q=" + q);

		if (ff != null && ff.size() > 0) {
			for (FFile f1 : ff) {
				xml += file2xml(code, f1, prop);
			}
		} else {
			xml += file2xml(code, f, prop);
		}

		if (FO.isInited()) {

			Task.schedule("fcloud.scan." + f.getFullname(), () -> {
				FO f1 = f.getFile_obj();
				if (f1 == null) {
					// remove all
				} else {
					f1.scan(2, -1);
				}

				// scan
				ff.parallelStream().forEach(e -> {
					FO f2 = e.getFile_obj();
					f2.scan(2, -1);

					if (e.getFile_obj() == null || !e.getFile_obj().exists()) {
						FFile.dao.delete(W.create("_path", e.getFullname()));
						FFile.dao.delete(e.getId());
					} else if (X.isSame("/", e.getFullname()) || !X.isSame(e.getName(), e.getFile_obj().getName())) {
						FFile.dao.delete(e.getId());
					}
				});
			});
		}

		return xml;

	}

	private String file2xml(Code code, FFile f1, JSON prop) throws IOException {
		String xml = X.EMPTY;
		xml += "<d:response><d:href>" + Global.getString("site.url", "http://127.0.0.1") + "/fc/files/"
				+ X.getCanonicalPath(code.getString("s2") + "/" + shortname(code, f1.getFullname())) + "</d:href>";

		xml += "<d:propstat><d:prop>";
		xml += "<d:displayname>" + f1.getName() + "</d:displayname>";
		if (prop != null && prop.containsKey("resourcetype")) {
			xml += "<d:resourcetype>";
			if (f1.isDir()) {
				xml += "<d:collection/>";
			}
			xml += "</d:resourcetype>";
		}

		if (prop != null && prop.containsKey("getlastmodified")) {
			xml += "<d:creationdate>" + lang.format(f1.getFupdated(), "yyyy-MM-dd HH:mm:ss") + "</d:creationdate>";
			xml += "<d:getlastmodified>" + lang.format(f1.getFupdated() - 8 * X.AHOUR, "E, dd M yyyy HH:mm:ss z")
					+ "</d:getlastmodified>";
		}

		if (prop != null && prop.containsKey("quota-available-bytes")) {

			Quato a = Quato.load(code.getLong("uid"));

			xml += "<d:quota-available-bytes>" + a.getFree() + "</d:quota-available-bytes>";
			xml += "<d:quota-used-bytes>" + a.getUsed() + "</d:quota-used-bytes>";
		}

		xml += "</d:prop>";
		xml += "<d:status>HTTP/1.1 200 OK</d:status></d:propstat>";

		if (prop != null && prop.containsKey("getcontentlength")) {
			xml += "<d:propstat><d:prop>";
			if (!f1.isDir()) {
				xml += "<d:getcontenttype>" + Controller.getMimeType(f1.getName()) + "</d:getcontenttype>";
			}
			xml += "<d:getcontentlength>" + f1.getLength() + "</d:getcontentlength>";
//			xml += "<d:checked-in></d:checked-in><d:checked-out></d:checked-out>";
			xml += "</d:prop>";
//			xml += "<d:status>HTTP/1.1 404 Not Found</d:status>";

			xml += "</d:propstat>";
		}

		xml += "</d:response>";
		return xml;
	}

	protected String determineMethodsAllowed(HttpServletRequest req) {

		StringBuilder methodsAllowed = new StringBuilder("OPTIONS, GET, POST, HEAD");

		methodsAllowed.append(", DELETE"); // RW
		methodsAllowed.append(", PUT"); // dir

		// Trace - assume disabled unless we can prove otherwise

		methodsAllowed.append(", LOCK, UNLOCK, PROPPATCH, COPY, MOVE");

		methodsAllowed.append(", PROPFIND");

		methodsAllowed.append(", MKCOL"); // not exists

		return methodsAllowed.toString();
	}

	/**
	 * Create a new lock.
	 */
	private static final int LOCK_CREATION = 0;

	/**
	 * Refresh lock.
	 */
	private static final int LOCK_REFRESH = 1;

}