/* * 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.ignite.igfs.secondary.local; import java.util.ArrayList; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.FileTime; import java.util.concurrent.TimeUnit; import org.apache.ignite.IgniteException; import org.apache.ignite.IgniteLogger; import org.apache.ignite.cluster.ClusterNode; import org.apache.ignite.igfs.IgfsBlockLocation; import org.apache.ignite.igfs.IgfsException; import org.apache.ignite.igfs.IgfsFile; import org.apache.ignite.igfs.IgfsPath; import org.apache.ignite.igfs.IgfsPathAlreadyExistsException; import org.apache.ignite.igfs.IgfsPathIsNotDirectoryException; import org.apache.ignite.igfs.IgfsPathNotFoundException; import org.apache.ignite.igfs.secondary.IgfsSecondaryFileSystem; import org.apache.ignite.igfs.secondary.IgfsSecondaryFileSystemPositionedReadable; import org.apache.ignite.internal.processors.igfs.IgfsDataManager; import org.apache.ignite.internal.processors.igfs.IgfsImpl; import org.apache.ignite.internal.processors.igfs.secondary.local.LocalFileSystemBlockKey; import org.apache.ignite.internal.processors.igfs.IgfsUtils; import org.apache.ignite.internal.processors.igfs.IgfsBlockLocationImpl; import org.apache.ignite.internal.processors.igfs.secondary.local.LocalFileSystemIgfsFile; import org.apache.ignite.internal.processors.igfs.secondary.local.LocalFileSystemSizeVisitor; import org.apache.ignite.internal.processors.igfs.secondary.local.LocalFileSystemUtils; import org.apache.ignite.internal.processors.igfs.secondary.local.LocalFileSystemPositionedReadable; import org.apache.ignite.internal.util.typedef.F; import org.apache.ignite.internal.util.typedef.internal.U; import org.apache.ignite.lifecycle.LifecycleAware; import org.apache.ignite.resources.FileSystemResource; import org.apache.ignite.resources.LoggerResource; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.PosixFileAttributes; import java.util.Collection; import java.util.Collections; import java.util.Map; /** * Secondary file system which delegates to local file system. */ public class LocalIgfsSecondaryFileSystem implements IgfsSecondaryFileSystem, LifecycleAware { /** Path that will be added to each passed path. */ private String workDir; /** Logger. */ @SuppressWarnings("unused") @LoggerResource private IgniteLogger log; /** IGFS instance. */ @SuppressWarnings("unused") @FileSystemResource private IgfsImpl igfs; /** * Heuristically checks if exception was caused by invalid HDFS version and returns appropriate exception. * * @param e Exception to check. * @param msg Detailed error message. * @return Appropriate exception. */ private IgfsException handleSecondaryFsError(IOException e, String msg) { if (e instanceof FileNotFoundException) return new IgfsPathNotFoundException(e); else return new IgfsException(msg, e); } /** {@inheritDoc} */ @Override public boolean exists(IgfsPath path) { return fileForPath(path).exists(); } /** {@inheritDoc} */ @Nullable @Override public IgfsFile update(IgfsPath path, Map<String, String> props) { File f = fileForPath(path); if (!f.exists()) return null; updatePropertiesIfNeeded(path, props); return info(path); } /** {@inheritDoc} */ @Override public void rename(IgfsPath src, IgfsPath dest) { File srcFile = fileForPath(src); File destFile = fileForPath(dest); if (!srcFile.exists()) throw new IgfsPathNotFoundException("Failed to perform rename because source path not found: " + src); if (srcFile.isDirectory() && destFile.isFile()) throw new IgfsPathIsNotDirectoryException("Failed to perform rename because destination path is " + "directory and source path is file [src=" + src + ", dest=" + dest + ']'); try { if (destFile.isDirectory()) Files.move(srcFile.toPath(), destFile.toPath().resolve(srcFile.getName())); else if(!srcFile.renameTo(destFile)) throw new IgfsException("Failed to perform rename (underlying file system returned false) " + "[src=" + src + ", dest=" + dest + ']'); } catch (IOException e) { throw handleSecondaryFsError(e, "Failed to rename [src=" + src + ", dest=" + dest + ']'); } } /** {@inheritDoc} */ @SuppressWarnings("ConstantConditions") @Override public boolean delete(IgfsPath path, boolean recursive) { File f = fileForPath(path); if (!recursive) return f.delete(); else return deleteRecursive(f, false); } /** * Delete directory recursively. * * @param f Directory. * @param deleteIfExists Ignore delete errors if the file doesn't exist. * @return {@code true} if successful. */ private boolean deleteRecursive(File f, boolean deleteIfExists) { BasicFileAttributes attrs; try { attrs = Files.readAttributes(f.toPath(), BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); } catch (IOException ignore) { return deleteIfExists && !f.exists(); } if (!attrs.isDirectory() || attrs.isSymbolicLink()) return f.delete() || (deleteIfExists && !f.exists()); File[] entries = f.listFiles(); if (entries != null) { for (File entry : entries) { boolean res = deleteRecursive(entry, true); if (!res) return false; } } return f.delete() || (deleteIfExists && !f.exists()); } /** {@inheritDoc} */ @Override public void mkdirs(IgfsPath path) { if (!mkdirs0(fileForPath(path))) throw new IgniteException("Failed to make directories (underlying file system returned false): " + path); } /** {@inheritDoc} */ @Override public void mkdirs(IgfsPath path, @Nullable Map<String, String> props) { mkdirs(path); updatePropertiesIfNeeded(path, props); } /** * Create directories. * * @param dir Directory. * @return Result. */ private boolean mkdirs0(@Nullable File dir) { if (dir == null) return true; // Nothing to create. if (dir.exists()) // Already exists, so no-op. return dir.isDirectory(); else { File parentDir = dir.getParentFile(); if (!mkdirs0(parentDir)) // Create parent first. return false; boolean res = dir.mkdir(); if (!res) res = dir.exists(); // Tolerate concurrent creation. return res; } } /** {@inheritDoc} */ @Override public Collection<IgfsPath> listPaths(IgfsPath path) { File[] entries = listFiles0(path); if (F.isEmpty(entries)) return Collections.emptySet(); else { Collection<IgfsPath> res = U.newHashSet(entries.length); for (File entry : entries) res.add(igfsPath(entry)); return res; } } /** {@inheritDoc} */ @Override public Collection<IgfsFile> listFiles(IgfsPath path) { File[] entries = listFiles0(path); if (F.isEmpty(entries)) return Collections.emptySet(); else { Collection<IgfsFile> res = U.newHashSet(entries.length); for (File entry : entries) { IgfsFile info = info(igfsPath(entry)); if (info != null) res.add(info); } return res; } } /** * Returns an array of File object. Under the specific path. * * @param path IGFS path. * @return Array of File objects. */ @Nullable private File[] listFiles0(IgfsPath path) { File f = fileForPath(path); if (!f.exists()) throw new IgfsPathNotFoundException("Failed to list files (path not found): " + path); else return f.listFiles(); } /** {@inheritDoc} */ @Override public IgfsSecondaryFileSystemPositionedReadable open(IgfsPath path, int bufSize) { try { FileInputStream in = new FileInputStream(fileForPath(path)); return new LocalFileSystemPositionedReadable(in, bufSize); } catch (IOException e) { throw handleSecondaryFsError(e, "Failed to open file for read: " + path); } } /** {@inheritDoc} */ @Override public OutputStream create(IgfsPath path, boolean overwrite) { return create0(path, overwrite); } /** {@inheritDoc} */ @Override public OutputStream create(IgfsPath path, int bufSize, boolean overwrite, int replication, long blockSize, @Nullable Map<String, String> props) { OutputStream os = create0(path, overwrite); try { updatePropertiesIfNeeded(path, props); return os; } catch (Exception err) { try { os.close(); } catch (IOException closeErr) { err.addSuppressed(closeErr); } throw err; } } /** {@inheritDoc} */ @Override public OutputStream append(IgfsPath path, int bufSize, boolean create, @Nullable Map<String, String> props) { try { File file = fileForPath(path); boolean exists = file.exists(); if (exists) { OutputStream os = new FileOutputStream(file, true); try { updatePropertiesIfNeeded(path, props); return os; } catch (Exception err) { try { os.close(); throw err; } catch (IOException closeErr) { err.addSuppressed(closeErr); throw err; } } } else { if (create) return create(path, bufSize, false, 0, 0, props); else throw new IgfsPathNotFoundException("Failed to append to file because it doesn't exist: " + path); } } catch (IOException e) { throw handleSecondaryFsError(e, "Failed to append to file because it doesn't exist: " + path); } } /** {@inheritDoc} */ @Override public IgfsFile info(final IgfsPath path) { File file = fileForPath(path); if (!file.exists()) return null; boolean isDir = file.isDirectory(); PosixFileAttributes attrs = LocalFileSystemUtils.posixAttributes(file); Map<String, String> props = LocalFileSystemUtils.posixAttributesToMap(attrs); BasicFileAttributes basicAttrs = LocalFileSystemUtils.basicAttributes(file); if (isDir) { return new LocalFileSystemIgfsFile(path, false, true, 0, basicAttrs.lastAccessTime().toMillis(), basicAttrs.lastModifiedTime().toMillis(), 0, props); } else { return new LocalFileSystemIgfsFile(path, file.isFile(), false, 0, basicAttrs.lastAccessTime().toMillis(), basicAttrs.lastModifiedTime().toMillis(), file.length(), props); } } /** {@inheritDoc} */ @Override public long usedSpaceSize() { Path p = fileForPath(IgfsPath.ROOT).toPath(); try { LocalFileSystemSizeVisitor visitor = new LocalFileSystemSizeVisitor(); Files.walkFileTree(p, visitor); return visitor.size(); } catch (IOException e) { throw new IgfsException("Failed to calculate used space size.", e); } } /** {@inheritDoc} */ @Override public void setTimes(IgfsPath path, long modificationTime, long accessTime) throws IgniteException { Path p = fileForPath(path).toPath(); if (!Files.exists(p)) throw new IgfsPathNotFoundException("Failed to set times (path not found): " + path); try { Files.getFileAttributeView(p, BasicFileAttributeView.class) .setTimes( (modificationTime >= 0) ? FileTime.from(modificationTime, TimeUnit.MILLISECONDS) : null, (accessTime >= 0) ? FileTime.from(accessTime, TimeUnit.MILLISECONDS) : null, null); } catch (IOException e) { throw new IgniteException("Failed to set times for path: " + path, e); } } /** {@inheritDoc} */ @Override public void start() throws IgniteException { if (workDir != null) workDir = new File(workDir).getAbsolutePath(); } /** {@inheritDoc} */ @Override public void stop() throws IgniteException { // No-op. } /** {@inheritDoc} */ @Override public Collection<IgfsBlockLocation> affinity(IgfsPath path, long start, long len, long maxLen) throws IgniteException { File f = fileForPath(path); if (!f.exists()) throw new IgfsPathNotFoundException("File not found: " + path); // Create fake block & fake affinity for blocks long blockSize = igfs.configuration().getBlockSize(); if (maxLen <= 0) maxLen = Long.MAX_VALUE; assert maxLen > 0 : "maxLen : " + maxLen; long end = start + len; Collection<IgfsBlockLocation> blocks = new ArrayList<>((int)(len / maxLen)); IgfsDataManager data = igfs.context().data(); Collection<ClusterNode> lastNodes = null; long lastBlockIdx = -1; IgfsBlockLocationImpl lastBlock = null; for (long offset = start; offset < end; ) { long blockIdx = offset / blockSize; // Each step is min of maxLen and end of block. long lenStep = Math.min( maxLen - (lastBlock != null ? lastBlock.length() : 0), (blockIdx + 1) * blockSize - offset); lenStep = Math.min(lenStep, end - offset); // Create fake affinity key to map blocks of secondary filesystem to nodes. LocalFileSystemBlockKey affKey = new LocalFileSystemBlockKey(path, blockIdx); if (blockIdx != lastBlockIdx) { Collection<ClusterNode> nodes = data.affinityNodes(affKey); if (!nodes.equals(lastNodes) && lastNodes != null && lastBlock != null) { blocks.add(lastBlock); lastBlock = null; } lastNodes = nodes; lastBlockIdx = blockIdx; } if(lastBlock == null) lastBlock = new IgfsBlockLocationImpl(offset, lenStep, lastNodes); else lastBlock.increaseLength(lenStep); if (lastBlock.length() == maxLen || lastBlock.start() + lastBlock.length() == end) { blocks.add(lastBlock); lastBlock = null; } offset += lenStep; } return blocks; } /** * Get work directory. * * @return Work directory. */ @Nullable public String getWorkDirectory() { return workDir; } /** * Set work directory. * * @param workDir Work directory. */ public void setWorkDirectory(@Nullable String workDir) { this.workDir = workDir; } /** * Create file for IGFS path. * * @param path IGFS path. * @return File object. */ private File fileForPath(IgfsPath path) { if (workDir == null) return new File(path.toString()); else { if ("/".equals(path.toString())) return new File(workDir); else return new File(workDir, path.toString()); } } /** * Create IGFS path for file. * * @param f File object. * @return IFGS path. * @throws IgfsException If failed. */ private IgfsPath igfsPath(File f) throws IgfsException { String path = f.getAbsolutePath(); if (workDir != null) { if (!path.startsWith(workDir)) throw new IgfsException("Path is not located in the work directory [workDir=" + workDir + ", path=" + path + ']'); path = path.substring(workDir.length(), path.length()); } return new IgfsPath(path); } /** * Internal create routine. * * @param path Path. * @param overwrite Overwrite flag. * @return Output stream. */ private OutputStream create0(IgfsPath path, boolean overwrite) { File file = fileForPath(path); boolean exists = file.exists(); if (exists) { if (!overwrite) throw new IgfsPathAlreadyExistsException("Failed to create a file because it already exists: " + path); } else { File parent = file.getParentFile(); if (!mkdirs0(parent)) throw new IgfsException("Failed to create parent directory for file (underlying file system " + "returned false): " + path); } try { return new FileOutputStream(file); } catch (IOException e) { throw handleSecondaryFsError(e, "Failed to create file [path=" + path + ", overwrite=" + overwrite + ']'); } } /** * Update path properties if needed. * * @param path IGFS path * @param props Properties map. */ private void updatePropertiesIfNeeded(IgfsPath path, Map<String, String> props) { if (props == null || props.isEmpty()) return; File file = fileForPath(path); if (!file.exists()) throw new IgfsPathNotFoundException("Failed to update properties for path: " + path); LocalFileSystemUtils.updateProperties(file, props.get(IgfsUtils.PROP_GROUP_NAME), props.get(IgfsUtils.PROP_PERMISSION)); } }