/** * personium.io * Copyright 2014 FUJITSU LIMITED * * 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.fujitsu.dc.core.model.file; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.SyncFailedException; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * ファイルシステムに対してWebDAVのバイナリファイルの入出力を行うアクセサクラス. */ public class BinaryDataAccessor { private static Logger logger = LoggerFactory.getLogger(BinaryDataAccessor.class); /** * Davファイルの読み書き時、ハードリンク作成/ファイル名改変時の最大リトライ回数. * ※本クラスは、Dc-Coreに含まれないため、dc-config.propertiesを参照できないものと考え、システムプロパティで処理を行うものとする */ private static int maxRetryCount = Integer.parseInt(System.getProperty( "com.fujitsu.dc.core.binaryData.dav.retry.count", "100")); /** * Davファイルの読み書き時、ハードリンク作成/ファイル名改変時のリトライ間隔(msec). * ※本クラスは、Dc-Coreに含まれないため、dc-config.propertiesを参照できないものと考え、システムプロパティで処理を行うものとする */ private static long retryInterval = Long.parseLong(System.getProperty( "com.fujitsu.dc.core.binaryData.dav.retry.interval", "50")); private static final int FILE_BUFFER_SIZE = 1024; private String baseDir; private String unitUserName; private boolean isPhysicalDeleteMode = false; private boolean fsyncEnabled = false; /** * コンストラクタ. * @param path 格納ディレクトリ * @param fsyncEnabled ファイル書き込み時にfsyncを有効にするか否か(true: 有効, false: 無効) */ public BinaryDataAccessor(String path, boolean fsyncEnabled) { this(path, null, fsyncEnabled); } /** * コンストラクタ. * @param path 格納ディレクトリ * @param unitUserName ユニットユーザ名 * @param fsyncEnabled ファイル書き込み時にfsyncを有効にするか否か(true: 有効, false: 無効) */ public BinaryDataAccessor(String path, String unitUserName, boolean fsyncEnabled) { this.baseDir = path; if (!this.baseDir.endsWith("/")) { this.baseDir += "/"; } this.unitUserName = unitUserName; this.fsyncEnabled = fsyncEnabled; } /** * コンストラクタ. * @param path 格納ディレクトリ * @param unitUserName ユニットユーザ名 * @param isPhysicalDeleteMode ファイル削除時に物理削除するか(true: 物理削除, false: 論理削除) * @param fsyncEnabled ファイル書き込み時にfsyncを有効にするか否か(true: 有効, false: 無効) */ public BinaryDataAccessor(String path, String unitUserName, boolean isPhysicalDeleteMode, boolean fsyncEnabled) { this.baseDir = path; if (!this.baseDir.endsWith("/")) { this.baseDir += "/"; } this.unitUserName = unitUserName; this.isPhysicalDeleteMode = isPhysicalDeleteMode; this.fsyncEnabled = fsyncEnabled; } /** * ファイル削除時に物理削除するかどうかの設定. * @return true: 物理削除, false: 論理削除 */ public boolean isPhysicalDeleteMode() { return isPhysicalDeleteMode; } /** * ストリームから読み込んだデータをファイルに書き込む. * @param inputStream 入力元のストリーム * @param filename ファイル名 * @throws BinaryDataAccessException ファイル出力で異常が発生した場合にスローする * @return 書き込んだバイト数 */ public long create(InputStream inputStream, String filename) throws BinaryDataAccessException { String directory = getSubDirectoryName(filename); String fullPathName = this.baseDir + directory + filename; createSubDirectories(this.baseDir + directory); return writeToTmpFile(inputStream, fullPathName); } /** * ストリームから読み込んだデータをファイルに書き込む. * @param inputStream 入力元のストリーム * @param filename ファイル名 * @throws BinaryDataAccessException ファイル入出力で異常が発生した場合にスローする * @return 書き込んだバイト数 */ public long update(InputStream inputStream, String filename) throws BinaryDataAccessException { String fullPathName = getFilePath(filename); if (!exists(fullPathName)) { throw new BinaryDataNotFoundException(fullPathName); } return writeToTmpFile(inputStream, fullPathName); } /** * ファイルをストリームにコピーする. * @param filename ファイル名 * @param outputStream コピー先ストリーム * @throws BinaryDataAccessException ファイル入出力で異常が発生した場合にスローする * @return コピーしたバイト数 */ public long copy(String filename, OutputStream outputStream) throws BinaryDataAccessException { String fullPathName = getFilePath(filename); if (!exists(fullPathName)) { throw new BinaryDataNotFoundException(fullPathName); } return writeToStream(fullPathName, outputStream); } /** * ファイルをストリームで取得する. * @param filename ファイル名 * @return ファイルのストリーム * @throws BinaryDataAccessException ファイル入出力で異常が発生した場合にスローする */ public InputStream getFileStream(String filename) throws BinaryDataAccessException { String fullPathName = getFilePath(filename); if (!exists(fullPathName)) { throw new BinaryDataNotFoundException(fullPathName); } try { FileInputStream fis = new FileInputStream(fullPathName); return new BufferedInputStream(fis); } catch (FileNotFoundException e) { throw new BinaryDataNotFoundException(fullPathName); } } /** * ファイルを削除する. 設定に従い、論理削除(デフォルト)/物理削除を行う 対象ファイルが存在しない場合は何もしない * @param filename ファイル名 * @throws BinaryDataAccessException ファイル入出力で異常が発生した場合にスローする */ public void delete(String filename) throws BinaryDataAccessException { String fullPathName = getFilePath(filename); deleteWithFullPath(fullPathName); } /** * ファイルを削除する(フルパス指定). 設定に従い、論理削除(デフォルト)/物理削除を行う 対象ファイルが存在しない場合は何もしない * @param filepath ファイルパス * @throws BinaryDataAccessException ファイル入出力で異常が発生した場合にスローする */ public void deleteWithFullPath(String filepath) throws BinaryDataAccessException { if (exists(filepath)) { if (this.isPhysicalDeleteMode) { deletePhysicalFileWithFullPath(filepath); } else { deleteFile(filepath); } } } /** * ファイルサイズを返す. * @param filename ファイル名 * @return ファイルサイズ(bytes) */ public long getSize(String filename) { String fullPathName = getFilePath(filename); return getFileSize(fullPathName); } /** * ファイル存在有無チェック. * @param filename ファイル名 * @return true:存在する、false:存在しない */ public boolean existsForFilename(String filename) { String fullPathName = getFilePath(filename); return exists(fullPathName); } /** * 一時ファイルをリネームする. * @param filename ファイル名 * @throws BinaryDataAccessException BinaryDataAccessException */ public void copyFile(String filename) throws BinaryDataAccessException { String fullPathName = getFilePath(filename); String tmpName = fullPathName + ".tmp"; File tmpFile = new File(tmpName); File dstFile = new File(fullPathName); if (!exists(tmpName)) { throw new BinaryDataNotFoundException(tmpName); } for (int i = 0; i < maxRetryCount; i++) { try { synchronized (fullPathName) { Files.move(tmpFile.toPath(), dstFile.toPath(), StandardCopyOption.ATOMIC_MOVE); } // 処理成功すれば、その場で復帰する。 return; } catch (IOException e) { logger.debug("Failed to copy file:" + tmpFile + " to " + dstFile + ". Will try again."); try { Thread.sleep(retryInterval); } catch (InterruptedException e2) { logger.debug("Thread interrupted."); } } } throw new BinaryDataAccessException("Failed to copy file:" + tmpFile + " to " + dstFile); } /** * ファイルを物理削除する. 対象ファイルが存在しない場合は何もしない * @param filename ファイル名 * @throws BinaryDataAccessException ファイル入出力で異常が発生した場合にスローする */ public void deletePhysicalFile(String filename) throws BinaryDataAccessException { String fullPathName = getFilePath(filename); if (exists(fullPathName)) { deletePhysicalFileWithFullPath(fullPathName); } } /** * ファイルを物理削除する. * @param filepath ファイル名(フルパス) * @throws BinaryDataAccessException ファイル入出力で異常が発生した場合にスローする */ private void deletePhysicalFileWithFullPath(String filepath) throws BinaryDataAccessException { Path file = new File(filepath).toPath(); for (int i = 0; i < maxRetryCount; i++) { try { synchronized (filepath) { Files.delete(file); } // 処理成功すれば、その場で復帰する。 return; } catch (IOException e) { logger.debug("Failed to delete file: " + filepath + ". Will retry again."); try { Thread.sleep(retryInterval); } catch (InterruptedException e2) { logger.debug("Thread interrupted."); } } } throw new BinaryDataAccessException("Failed to delete file: " + filepath); } /** * ファイル名からファイルのフルパスを取得する. * @param filename ファイル名 * @return ファイルフルパス */ public String getFilePath(String filename) { String directory = getSubDirectoryName(filename); String fullPathName = this.baseDir + directory + filename; return fullPathName; } private static final int SUBDIR_NAME_LEN = 2; private boolean exists(String fullPathFilename) { File file = new File(fullPathFilename); return file.exists(); } private String getSubDirectoryName(String filename) { StringBuilder sb = new StringBuilder(""); if (this.unitUserName != null) { sb.append(this.unitUserName); sb.append("/"); } sb.append(splitDirectoryName(filename, 0)); sb.append("/"); sb.append(splitDirectoryName(filename, SUBDIR_NAME_LEN)); sb.append("/"); return sb.toString(); } private String splitDirectoryName(String filename, int index) { return filename.substring(index, index + SUBDIR_NAME_LEN); } private void createSubDirectories(String directory) throws BinaryDataAccessException { File newDir = new File(directory); // 既にディレクトリがあれば、何もしない if (!newDir.exists()) { try { Files.createDirectories(newDir.toPath()); } catch (IOException e) { throw new BinaryDataAccessException("DirectoryCreateFailed:" + directory, e); } } } private long writeToTmpFile(InputStream inputStream, String fullPathName) throws BinaryDataAccessException { FileOutputStream outputStream = null; String tmpfileName = fullPathName + ".tmp"; try { outputStream = new FileOutputStream(tmpfileName); return copyStream(inputStream, outputStream); } catch (IOException ex) { throw new BinaryDataAccessException("WriteToFileFailed:" + tmpfileName, ex); } finally { closeOutputStream(outputStream); } } private long writeToStream(String fullPathName, OutputStream outputStream) throws BinaryDataAccessException { FileInputStream inputStream = null; try { inputStream = new FileInputStream(fullPathName); return copyStream(inputStream, outputStream); } catch (FileNotFoundException e) { throw new BinaryDataNotFoundException(fullPathName); } catch (BinaryDataAccessException ex) { throw new BinaryDataAccessException("WriteToStreamFailed:" + fullPathName, ex); } finally { closeInputStream(inputStream); } } private long copyStream(InputStream inputStream, OutputStream outputStream) throws BinaryDataAccessException { BufferedInputStream bufferedInput = null; BufferedOutputStream bufferedOutput = null; try { bufferedInput = new BufferedInputStream(inputStream); bufferedOutput = new BufferedOutputStream(outputStream); byte[] buf = new byte[FILE_BUFFER_SIZE]; long totalBytes = 0L; int len; while ((len = bufferedInput.read(buf)) != -1) { bufferedOutput.write(buf, 0, len); totalBytes += len; } return totalBytes; } catch (IOException ex) { throw new BinaryDataAccessException("CopyStreamFailed.", ex); } finally { closeOutputStream(bufferedOutput); closeInputStream(bufferedInput); } } private void closeInputStream(InputStream inputStream) { try { if (inputStream != null) { inputStream.close(); } } catch (IOException ex) { logger.debug("StreamCloseFailed:" + ex.getMessage()); } } private void closeOutputStream(OutputStream outputStream) { try { if (outputStream != null) { outputStream.flush(); if (this.fsyncEnabled) { fsyncIfFileOutputStream(outputStream); } outputStream.close(); } } catch (IOException ex) { logger.debug("StreamCloseFailed:" + ex.getMessage()); } } /** * ファイルディスクリプタの同期. * @param fd ファイルディスクリプタ * @exception SyncFailedException 同期に失敗 */ public void sync(FileDescriptor fd) throws SyncFailedException { fd.sync(); } private void fsyncIfFileOutputStream(OutputStream outputStream) throws IOException { if (outputStream instanceof FileOutputStream) { FileDescriptor desc = ((FileOutputStream) outputStream).getFD(); if (null != desc && desc.valid()) { sync(desc); } } else if (outputStream instanceof FilterOutputStream) { // FilterOutputStream の場合には、"out"field から FileOutputStream を取り出してfsyncする fsyncIfFileOutputStream(getInternalOutputStream(((FilterOutputStream) outputStream))); } } private OutputStream getInternalOutputStream(FilterOutputStream sourceOutputStream) { if (null != sourceOutputStream) { try { Field internalOut; internalOut = FilterOutputStream.class.getDeclaredField("out"); internalOut.setAccessible(true); Object out = internalOut.get(sourceOutputStream); if (out instanceof OutputStream) { return (OutputStream) out; } } catch (NoSuchFieldException e) { return null; } catch (SecurityException e) { return null; } catch (IllegalArgumentException e) { return null; } catch (IllegalAccessException e) { return null; } } return null; } private void deleteFile(String srcFullPathName) throws BinaryDataAccessException { String dstFullPathName = srcFullPathName + ".deleted"; File srcFile = new File(srcFullPathName); File dstFile = new File(dstFullPathName); for (int i = 0; i < maxRetryCount; i++) { try { synchronized (srcFullPathName) { Files.move(srcFile.toPath(), dstFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } // 処理成功すれば、その場で復帰する。 return; } catch (IOException e) { logger.debug("Failed to delete file: " + srcFullPathName); try { Thread.sleep(retryInterval); } catch (InterruptedException e2) { logger.debug("Thread interrupted."); } } } throw new BinaryDataAccessException("Failed to delete file: " + srcFullPathName); } private long getFileSize(String fullPathName) { File file = new File(fullPathName); return file.length(); } }