Windows ファイル共有をJavaから利用する
ローカルPC環境でWebアプリケーション開発をしている時点では、OSにログインしているユーザーがエクスプローラーから他ホストの共有ディレクトリが利用できる状態であれば、問題なく他ホストの共有ディレクトリをWebアプリケーションから利用することができる。
java.io.File クラスは、Windows の UNCパスの接頭辞にも対応している。
しかしながら、開発サーバーにデプロイすると、OSにログインしているユーザーが、問題なく他ホストの共有ディレクトリを利用できる状態であったとしても、他ホストの共有ディレクトリがアプリケーションから見えなくなってしまう。
どうやら、これは、アプリケーションサーバーを実行しているユーザーによるようで、Windows サービスとしてアプリケーションサーバーが実行されていると、サービスを実行しているユーザーの権限では、共有ディレクトリが見えなくなるようだ。
Java の場合、困ってもたいていの場合、OSSの解決策があって助かるが、例に漏れず、JCIFS というライブラリが存在するので、それを利用する。
ちなみに、CIFS とは、Windows ファイル共有の公開APIのことだそうだ。
しかしながら、このライブラリ幾分使い勝手が悪い。
こちらは、java.io.File と jcifs.smb.SmbFile をポリモーフィックに使いたいのだが。。。使えない。
だって、お互い全然関係ないんだもの。
java.io.File がインターフェースでない以上、java.io.File を継承して SmbFile を作成するのはおかしい。。。~しょうが無いかな。java.io.File は final クラスではないとはいえ。
Java SE 7 からは、java.nio.Path インターフェースあたりが、その責を果たすのかしら?勉強不足でわかりません。
ということで、File を継承して、SmbFIle とミックスしたクラスをつくったり、ラッパークラスをつくるほどでもないので、ユーティリティを作成して対処することに。以下、次の時のことを考えてメモっておく。
package info.typea.util; import java.io.BufferedInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.net.MalformedURLException; import jcifs.smb.SmbException; import jcifs.smb.SmbFile; import jcifs.smb.SmbFileOutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * ファイルシステムユーティリティ * */ public class FileSystemUtils { protected static Logger logger = LoggerFactory.getLogger(FileSystemUtils.class); private static final int BUF_SIZE = 4096; private FileSystemUtils(){} /** * 通常のファイルシステムもしくは CIFS(Windowsファイル共有サービス) にファイルを書き込む * * @param is 書き込み内容 * @param file 書き込み対象ファイル * @param isCifs CIFS か否か * @return */ public static FileSystemUtilInfo writeToFile( InputStream is, File file, boolean isCifs, String user, String password) { if (isCifs) { return writeToCifsFile(is, file, user, password); } else { return writeToLocalFile(is, file); } } /** * java.io.File から、SmbFile オブジェクトを生成する * * @param targetFile * @param user * @param password * @return */ public static SmbFile createSmbFileFromFile( File targetFile, String user, String password) { SmbFile file = null; try { file = new SmbFile( cifsUrl( user, cisfPassword(password), cisfPath(targetFile.getAbsolutePath()))); } catch (MalformedURLException e) { // TODO } return file; } /** * 通常のファイルシステムにファイルを作成 * * @param is * @param targetFile * @return */ private static FileSystemUtilInfo writeToLocalFile(InputStream is, File targetFile) { FileSystemUtilInfo info = new FileSystemUtilInfo(); FileOutputStream fos = null; try { fos = new FileOutputStream(targetFile); BufferedInputStream bis = new BufferedInputStream(is); int len = 0; long fileSize = 0; byte[] buffer = new byte[BUF_SIZE]; while ((len = bis.read(buffer)) >= 0) { fos.write(buffer,0, len); fileSize += len; } info.setFileSize(fileSize); bis.close(); } catch(Exception e) { throw new IllegalStateException(e); } finally { try { fos.close(); } catch (Exception e2) {} } return info; } /** * CIFS ファイルシステムにファイルを作成 * * @param is * @param targetFile * @return * @see http://jcifs.samba.org/src/docs/api/jcifs/smb/SmbFile.html */ private static FileSystemUtilInfo writeToCifsFile( InputStream is, File targetFile, String user, String password) { FileSystemUtilInfo info = new FileSystemUtilInfo(); try { makeCifsDirectories(targetFile.getParentFile(), user, password); SmbFile file = new SmbFile( cifsUrl( user, cisfPassword(password), cisfPath(targetFile.getAbsolutePath()))); if (!file.exists()) { file.createNewFile(); } SmbFileOutputStream fos = new SmbFileOutputStream(file); BufferedInputStream bis = new BufferedInputStream(is); int len = 0; long fileSize = 0; byte[] buffer = new byte[BUF_SIZE]; while ((len = bis.read(buffer)) >= 0) { fos.write(buffer,0, len); fileSize += len; } info.setFileSize(fileSize); bis.close(); } catch (Exception e) { // TODO } return info; } /** * CIFS ディレクトリが存在しない場合、作成する * * @param dir * @param user * @param password * @throws MalformedURLException * @throws SmbException */ private static void makeCifsDirectories( File dir, String user, String password) throws MalformedURLException, SmbException { SmbFile cifsDir = new SmbFile( cifsUrl( user, cisfPassword(password), cisfPath(dir.getAbsolutePath()))); if (!cifsDir.exists()) { cifsDir.mkdirs(); } } /** * CIFS アクセス URL * @param user * @param password * @param path * @return */ private static String cifsUrl(String user, String password, String path) { return String.format("smb://%s:%s@%s",user,password,path); } /** * '@' is URL encoded with the '%40' hexcode escape * * @param password * @return */ private static String cisfPassword(String password) { return password.replaceAll("@", "%40"); } /** * "\\" で始まる、UNCパス を "server/share/path/to/file.txt" のような書式に変換 * * @param path * @return */ private static String cisfPath(String path) { // \\ を取り除く if (path.substring(0,2).equals("\\\\")) { path = path.substring(2); } return path.replaceAll("\\\\", "\\/"); } /** * 結果情報 * */ public static class FileSystemUtilInfo { private long fileSize; public long getFileSize() { return fileSize; } public void setFileSize(long fileSize) { this.fileSize = fileSize; } } }
んで、JAX-RS を利用していると、java.io.File は Response に含めることができるのだが、SmbFile は当然の様にできないので、MessageBodyWriter を実装したクラスを作成する必要が有る。こちらもメモ。
package info.typea.provider; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import javax.ws.rs.Consumes; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.ext.MessageBodyWriter; import javax.ws.rs.ext.Provider; import jcifs.smb.SmbException; import jcifs.smb.SmbFile; import jcifs.smb.SmbFileInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Windows ファイル共有 サービス(CIFS) ファイル 用 JAX-RS プロバイダー * * @see org.apache.wink.common.internal.providers.entity.FileProvider */ @Provider @Produces({ "*/*" }) @Consumes({ "*/*" }) public class SmbFileProvider implements MessageBodyWriter<SmbFile> { private static final Logger logger = LoggerFactory.getLogger(SmbFileProvider.class); @Override public long getSize(SmbFile t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { try { return t.length(); } catch (SmbException e) { return 0; } } @Override public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return SmbFile.class.isAssignableFrom(type); } @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public void writeTo( SmbFile t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { try { final SmbFile smbFile = t; final OutputStream os = entityStream; AccessController.doPrivileged(new PrivilegedExceptionAction() { public Object run() throws IOException { if ((!(smbFile.canRead())) || (smbFile.isDirectory())) { if (SmbFileProvider.logger.isWarnEnabled()) { SmbFileProvider.logger.error(String .format("cannotUseFileAsResponse %s", smbFile.getPath())); } throw new WebApplicationException(); } SmbFileInputStream fis = new SmbFileInputStream(smbFile); try { SmbFileProvider.this.pipe(fis, os); } finally { fis.close(); } return null; } }); } catch (PrivilegedActionException e) { if (e.getException() instanceof IOException) throw ((IOException) e.getException()); if (e.getException() instanceof WebApplicationException) throw ((WebApplicationException) e.getException()); throw new WebApplicationException(e.getException()); } } private void pipe(InputStream is, OutputStream os) throws IOException { byte[] ba = new byte[1024]; int i = is.read(ba); while (i != -1) { os.write(ba, 0, i); i = is.read(ba); } } }
これで、Windows共有ファイルを、JAX-RS 経由で利用することができる様になった。