├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src └── main ├── java └── org │ └── dcache │ └── simplenfs │ ├── App.java │ ├── LocalFileSystem.java │ └── SimpleNfsServer.java └── resources ├── exports ├── logback-test.xml └── logback.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea/ 3 | *.iml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simple NFS server 2 | ----------------- 3 | 4 | This is a demo/testing/playgourd code on top of dCache's [nfs4j](https://github.com/dcache/nfs4j) 5 | 6 | 7 | How to contribute 8 | ================= 9 | 10 | **simple-nfs** uses the linux kernel model where git is not only source repository, 11 | but also the way to track contributions and copyrights. 12 | 13 | Each submitted patch must have a "Signed-off-by" line. Patches without 14 | this line will not be accepted. 15 | 16 | The sign-off is a simple line at the end of the explanation for the 17 | patch, which certifies that you wrote it or otherwise have the right to 18 | pass it on as an open-source patch. The rules are pretty simple: if you 19 | can certify the below: 20 | ``` 21 | 22 | Developer's Certificate of Origin 1.1 23 | 24 | By making a contribution to this project, I certify that: 25 | 26 | (a) The contribution was created in whole or in part by me and I 27 | have the right to submit it under the open source license 28 | indicated in the file; or 29 | 30 | (b) The contribution is based upon previous work that, to the best 31 | of my knowledge, is covered under an appropriate open source 32 | license and I have the right under that license to submit that 33 | work with modifications, whether created in whole or in part 34 | by me, under the same open source license (unless I am 35 | permitted to submit under a different license), as indicated 36 | in the file; or 37 | 38 | (c) The contribution was provided directly to me by some other 39 | person who certified (a), (b) or (c) and I have not modified 40 | it. 41 | 42 | (d) I understand and agree that this project and the contribution 43 | are public and that a record of the contribution (including all 44 | personal information I submit with it, including my sign-off) is 45 | maintained indefinitely and may be redistributed consistent with 46 | this project or the open source license(s) involved. 47 | 48 | ``` 49 | then you just add a line saying ( git commit -s ) 50 | 51 | Signed-off-by: Random J Developer 52 | 53 | using your real name (sorry, no pseudonyms or anonymous contributions.) 54 | 55 | 56 | LICENSE 57 | ------- 58 | This work is published under [Apache v2](https://www.apache.org/licenses/) license. 59 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | org.dcache 6 | simple-nfs 7 | 1.0-SNAPSHOT 8 | 9 | Simple nfs server 10 | http://www.dcache.org/ 11 | 12 | 13 | UTF-8 14 | 15 | 16 | 17 | 18 | 19 | org.apache.maven.plugins 20 | maven-compiler-plugin 21 | 3.8.0 22 | 23 | 11 24 | 11 25 | 26 | 27 | 28 | org.apache.maven.plugins 29 | maven-assembly-plugin 30 | 3.1.0 31 | 32 | 33 | 34 | single 35 | 36 | package 37 | 38 | 39 | jar-with-dependencies 40 | 41 | 42 | 43 | org.dcache.simplenfs.App 44 | org.dcache.simplenfs 45 | 46 | 47 | 48 | development 49 | org.dcache.simplenfs 50 | ${project.version} 51 | ${buildNumber} 52 | dCache 53 | ${project.url} 54 | ${maven.build.timestamp} 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | org.dcache 67 | nfs4j-core 68 | 0.25.0 69 | 70 | 71 | org.slf4j 72 | slf4j-api 73 | 1.7.26 74 | 75 | 76 | org.slf4j 77 | log4j-over-slf4j 78 | 1.7.26 79 | 80 | 81 | ch.qos.logback 82 | logback-classic 83 | 1.2.13 84 | 85 | 86 | args4j 87 | args4j 88 | 2.33 89 | 90 | 91 | com.boundary 92 | high-scale-lib 93 | 1.0.6 94 | 95 | 96 | junit 97 | junit 98 | 4.13.1 99 | test 100 | 101 | 102 | 103 | 104 | 105 | dcache releases 106 | https://download.dcache.org/nexus/content/repositories/releases 107 | 108 | true 109 | 110 | 111 | false 112 | 113 | 114 | 115 | dcache snapshots 116 | https://download.dcache.org/nexus/content/repositories/snapshots 117 | 118 | false 119 | 120 | 121 | true 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /src/main/java/org/dcache/simplenfs/App.java: -------------------------------------------------------------------------------- 1 | package org.dcache.simplenfs; 2 | 3 | import org.kohsuke.args4j.CmdLineException; 4 | import org.kohsuke.args4j.CmdLineParser; 5 | import org.kohsuke.args4j.Option; 6 | 7 | import java.io.IOException; 8 | import java.nio.file.Path; 9 | import org.dcache.nfs.ExportFile; 10 | import org.dcache.oncrpc4j.portmap.OncRpcEmbeddedPortmap; 11 | 12 | /** 13 | * 14 | */ 15 | public class App { 16 | 17 | @Option(name = "-root", usage = "root of the file system to export", metaVar = "") 18 | private Path root; 19 | @Option(name = "-exports", usage = "path to file with export tables", metaVar = "") 20 | private Path exportsFile; 21 | @Option(name = "-nfsvers", usage = "NFS version (3, 4, 0==3+4) to use", metaVar = "") 22 | private int nfsVers = 0; 23 | @Option(name = "-port", usage = "TCP port to use", metaVar = "") 24 | private int rpcPort = 2049; 25 | @Option(name = "-with-portmap", usage = "start embedded portmap") 26 | private boolean withPortmap; 27 | 28 | public static void main(String[] args) throws Exception { 29 | new App().run(args); 30 | } 31 | 32 | public void run(String[] args) throws CmdLineException, IOException { 33 | 34 | CmdLineParser parser = new CmdLineParser(this); 35 | 36 | try { 37 | parser.parseArgument(args); 38 | } catch (CmdLineException e) { 39 | System.err.println(); 40 | System.err.println(e.getMessage()); 41 | System.err.println("Usage:"); 42 | System.err.println(" App [options...]"); 43 | System.err.println(); 44 | parser.printUsage(System.err); 45 | System.exit(1); 46 | } 47 | 48 | ExportFile exportFile = null; 49 | if (exportsFile != null) { 50 | exportFile = new ExportFile(exportsFile.toFile()); 51 | } 52 | 53 | if (withPortmap) { 54 | new OncRpcEmbeddedPortmap(); 55 | } 56 | 57 | try (SimpleNfsServer ignored = new SimpleNfsServer(nfsVers, rpcPort, root, exportFile, null)) { 58 | //noinspection ResultOfMethodCallIgnored 59 | System.in.read(); //any key to shutdown 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/org/dcache/simplenfs/LocalFileSystem.java: -------------------------------------------------------------------------------- 1 | package org.dcache.simplenfs; 2 | 3 | import com.google.common.primitives.Longs; 4 | import com.sun.security.auth.UnixNumericGroupPrincipal; 5 | import com.sun.security.auth.UnixNumericUserPrincipal; 6 | import org.cliffc.high_scale_lib.NonBlockingHashMap; 7 | import org.cliffc.high_scale_lib.NonBlockingHashMapLong; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import org.dcache.nfs.FsExport; 12 | import org.dcache.nfs.status.ExistException; 13 | import org.dcache.nfs.status.NoEntException; 14 | import org.dcache.nfs.status.NotEmptyException; 15 | import org.dcache.nfs.v4.NfsIdMapping; 16 | import org.dcache.nfs.v4.SimpleIdMap; 17 | import org.dcache.nfs.v4.xdr.nfsace4; 18 | import org.dcache.nfs.vfs.AclCheckable; 19 | import org.dcache.nfs.vfs.DirectoryEntry; 20 | import org.dcache.nfs.vfs.FsStat; 21 | import org.dcache.nfs.vfs.Inode; 22 | import org.dcache.nfs.vfs.Stat; 23 | import org.dcache.nfs.vfs.Stat.Type; 24 | import org.dcache.nfs.vfs.VirtualFileSystem; 25 | 26 | import javax.security.auth.Subject; 27 | 28 | import java.io.IOException; 29 | import java.io.RandomAccessFile; 30 | import java.nio.ByteBuffer; 31 | import java.nio.channels.FileChannel; 32 | import java.nio.file.DirectoryNotEmptyException; 33 | import java.nio.file.FileAlreadyExistsException; 34 | import java.nio.file.FileStore; 35 | import java.nio.file.FileSystems; 36 | import java.nio.file.FileVisitResult; 37 | import java.nio.file.Files; 38 | import java.nio.file.Path; 39 | import java.nio.file.SimpleFileVisitor; 40 | import java.nio.file.StandardCopyOption; 41 | import java.nio.file.StandardOpenOption; 42 | import java.nio.file.attribute.BasicFileAttributes; 43 | import java.nio.file.attribute.BasicFileAttributeView; 44 | import java.nio.file.attribute.DosFileAttributeView; 45 | import java.nio.file.attribute.DosFileAttributes; 46 | import java.nio.file.attribute.FileTime; 47 | import java.nio.file.attribute.GroupPrincipal; 48 | import java.nio.file.attribute.PosixFileAttributeView; 49 | import java.nio.file.attribute.UserPrincipal; 50 | import java.nio.file.attribute.UserPrincipalLookupService; 51 | import java.security.Principal; 52 | import java.util.ArrayList; 53 | import java.util.List; 54 | import java.util.concurrent.atomic.AtomicLong; 55 | 56 | import org.dcache.nfs.status.NotSuppException; 57 | import org.dcache.nfs.status.PermException; 58 | import org.dcache.nfs.status.ServerFaultException; 59 | import org.dcache.nfs.vfs.DirectoryStream; 60 | 61 | import static java.nio.file.LinkOption.NOFOLLOW_LINKS; 62 | 63 | /** 64 | * 65 | */ 66 | public class LocalFileSystem implements VirtualFileSystem { 67 | 68 | private static final Logger LOG = LoggerFactory.getLogger(LocalFileSystem.class); 69 | 70 | private final Path _root; 71 | private final NonBlockingHashMapLong inodeToPath = new NonBlockingHashMapLong<>(); 72 | private final NonBlockingHashMap pathToInode = new NonBlockingHashMap<>(); 73 | private final AtomicLong fileId = new AtomicLong(1); //numbering starts at 1 74 | private final NfsIdMapping _idMapper = new SimpleIdMap(); 75 | private final UserPrincipalLookupService _lookupService = 76 | FileSystems.getDefault().getUserPrincipalLookupService(); 77 | 78 | private final static boolean IS_UNIX; 79 | static { 80 | IS_UNIX = !System.getProperty("os.name").startsWith("Win"); 81 | } 82 | 83 | private Inode toFh(long inodeNumber) { 84 | return Inode.forFile(Longs.toByteArray(inodeNumber)); 85 | } 86 | 87 | private long getInodeNumber(Inode inode) { 88 | return Longs.fromByteArray(inode.getFileId()); 89 | } 90 | 91 | private Path resolveInode(long inodeNumber) throws NoEntException { 92 | Path path = inodeToPath.get(inodeNumber); 93 | if (path == null) { 94 | throw new NoEntException("inode #" + inodeNumber); 95 | } 96 | return path; 97 | } 98 | 99 | private long resolvePath(Path path) throws NoEntException { 100 | Long inodeNumber = pathToInode.get(path); 101 | if (inodeNumber == null) { 102 | throw new NoEntException("path " + path); 103 | } 104 | return inodeNumber; 105 | } 106 | 107 | /** 108 | * Map an inode number to a path. 109 | * @param inodeNumber the inode number 110 | * @param path the path 111 | * @param force if true, overwrite any existing mapping 112 | */ 113 | private void map(long inodeNumber, Path path, boolean force) { 114 | if (inodeToPath.putIfAbsent(inodeNumber, path) != null) { 115 | throw new IllegalStateException(); 116 | } 117 | 118 | if (force) { 119 | pathToInode.put(path, inodeNumber); 120 | } else { 121 | Long otherInodeNumber = pathToInode.putIfAbsent(path, inodeNumber); 122 | if (otherInodeNumber != null) { 123 | //try rollback 124 | if (inodeToPath.remove(inodeNumber) != path) { 125 | throw new IllegalStateException("cant map, rollback failed"); 126 | } 127 | throw new IllegalStateException("path " + path + " already mapped to " + otherInodeNumber); 128 | } 129 | } 130 | } 131 | 132 | private void map(long inodeNumber, Path path) { 133 | map(inodeNumber, path, false); 134 | } 135 | 136 | private void unmap(long inodeNumber, Path path) { 137 | Path removedPath = inodeToPath.remove(inodeNumber); 138 | if (!path.equals(removedPath)) { 139 | throw new IllegalStateException(); 140 | } 141 | if (pathToInode.remove(path) != inodeNumber) { 142 | throw new IllegalStateException(); 143 | } 144 | } 145 | 146 | private void remap(long inodeNumber, Path oldPath, Path newPath) { 147 | //TODO - attempt rollback? 148 | unmap(inodeNumber, oldPath); 149 | map(inodeNumber, newPath, true); 150 | } 151 | 152 | public LocalFileSystem(Path root, Iterable exportIterable) throws IOException { 153 | _root = root; 154 | assert (Files.exists(_root)); 155 | for (FsExport export : exportIterable) { 156 | String relativeExportPath = export.getPath().substring(1); // remove the opening '/' 157 | Path exportRootPath = root.resolve(relativeExportPath); 158 | if (!Files.exists(exportRootPath)) { 159 | Files.createDirectories(exportRootPath); 160 | } 161 | } 162 | //map existing structure (if any) 163 | map(fileId.getAndIncrement(), _root); //so root is always inode #1 164 | Files.walkFileTree(_root, new SimpleFileVisitor() { 165 | @Override 166 | public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { 167 | FileVisitResult superRes = super.preVisitDirectory(dir, attrs); 168 | if (superRes != FileVisitResult.CONTINUE) { 169 | return superRes; 170 | } 171 | if (dir.equals(_root)) { 172 | return FileVisitResult.CONTINUE; 173 | } 174 | map(fileId.getAndIncrement(), dir); 175 | return FileVisitResult.CONTINUE; 176 | } 177 | 178 | @Override 179 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { 180 | FileVisitResult superRes = super.visitFile(file, attrs); 181 | if (superRes != FileVisitResult.CONTINUE) { 182 | return superRes; 183 | } 184 | map(fileId.getAndIncrement(), file); 185 | return FileVisitResult.CONTINUE; 186 | } 187 | }); 188 | } 189 | 190 | @Override 191 | public Inode create(Inode parent, Type type, String path, Subject subject, int mode) throws IOException { 192 | long parentInodeNumber = getInodeNumber(parent); 193 | Path parentPath = resolveInode(parentInodeNumber); 194 | Path newPath = parentPath.resolve(path); 195 | try { 196 | Files.createFile(newPath); 197 | } catch (FileAlreadyExistsException e) { 198 | throw new ExistException("path " + newPath); 199 | } 200 | long newInodeNumber = fileId.getAndIncrement(); 201 | map(newInodeNumber, newPath); 202 | setOwnershipAndMode(newPath, subject, mode); 203 | return toFh(newInodeNumber); 204 | } 205 | 206 | @Override 207 | public FsStat getFsStat() throws IOException { 208 | FileStore store = Files.getFileStore(_root); 209 | long total = store.getTotalSpace(); 210 | long free = store.getUsableSpace(); 211 | return new FsStat(total, Long.MAX_VALUE, total-free, pathToInode.size()); 212 | } 213 | 214 | @Override 215 | public Inode getRootInode() throws IOException { 216 | return toFh(1); //always #1 (see constructor) 217 | } 218 | 219 | @Override 220 | public Inode lookup(Inode parent, String path) throws IOException { 221 | //TODO - several issues 222 | //2. we might accidentally allow composite paths here ("/dome/dir/down") 223 | //3. we dont actually check that the parent exists 224 | long parentInodeNumber = getInodeNumber(parent); 225 | Path parentPath = resolveInode(parentInodeNumber); 226 | Path child; 227 | if(path.equals(".")) { 228 | child = parentPath; 229 | } else if(path.equals("..")) { 230 | child = parentPath.getParent(); 231 | } else { 232 | child = parentPath.resolve(path); 233 | } 234 | long childInodeNumber = resolvePath(child); 235 | return toFh(childInodeNumber); 236 | } 237 | 238 | @Override 239 | public Inode link(Inode parent, Inode existing, String target, Subject subject) throws IOException { 240 | long parentInodeNumber = getInodeNumber(parent); 241 | Path parentPath = resolveInode(parentInodeNumber); 242 | 243 | long existingInodeNumber = getInodeNumber(existing); 244 | Path existingPath = resolveInode(existingInodeNumber); 245 | 246 | Path targetPath = parentPath.resolve(target); 247 | 248 | try { 249 | Files.createLink(targetPath, existingPath); 250 | } catch (UnsupportedOperationException e) { 251 | throw new NotSuppException("Not supported", e); 252 | } catch (FileAlreadyExistsException e) { 253 | throw new ExistException("Path exists " + target, e); 254 | } catch (SecurityException e) { 255 | throw new PermException("Permission denied: " + e.getMessage(), e); 256 | } catch (IOException e) { 257 | throw new ServerFaultException("Failed to create: " + e.getMessage(), e); 258 | } 259 | 260 | long newInodeNumber = fileId.getAndIncrement(); 261 | map(newInodeNumber, targetPath); 262 | return toFh(newInodeNumber); 263 | } 264 | 265 | @Override 266 | public DirectoryStream list(Inode inode, byte[] bytes, long l) throws IOException { 267 | long inodeNumber = getInodeNumber(inode); 268 | Path path = resolveInode(inodeNumber); 269 | final List list = new ArrayList<>(); 270 | try (java.nio.file.DirectoryStream ds = Files.newDirectoryStream(path)) { 271 | int cookie = 2; // first allowed cookie 272 | for (Path p : ds) { 273 | cookie++; 274 | if (cookie > l) { 275 | long ino = resolvePath(p); 276 | list.add(new DirectoryEntry(p.getFileName().toString(), toFh(ino), statPath(p, ino), cookie)); 277 | } 278 | } 279 | } 280 | return new DirectoryStream(list); 281 | } 282 | 283 | @Override 284 | public byte[] directoryVerifier(Inode inode) throws IOException { 285 | return DirectoryStream.ZERO_VERIFIER; 286 | } 287 | 288 | @Override 289 | public Inode mkdir(Inode parent, String path, Subject subject, int mode) throws IOException { 290 | long parentInodeNumber = getInodeNumber(parent); 291 | Path parentPath = resolveInode(parentInodeNumber); 292 | Path newPath = parentPath.resolve(path); 293 | try { 294 | Files.createDirectory(newPath); 295 | } catch (FileAlreadyExistsException e) { 296 | throw new ExistException("path " + newPath); 297 | } 298 | long newInodeNumber = fileId.getAndIncrement(); 299 | map(newInodeNumber, newPath); 300 | setOwnershipAndMode(newPath, subject, mode); 301 | return toFh(newInodeNumber); 302 | } 303 | 304 | private void setOwnershipAndMode(Path target, Subject subject, int mode) 305 | { 306 | if (!IS_UNIX) { 307 | // FIXME: windows must support some kind of file owhership as well 308 | return; 309 | } 310 | 311 | int uid = -1; 312 | int gid = -1; 313 | for (Principal principal : subject.getPrincipals()) { 314 | if (principal instanceof UnixNumericUserPrincipal) { 315 | uid = (int) ((UnixNumericUserPrincipal)principal).longValue(); 316 | } 317 | if (principal instanceof UnixNumericGroupPrincipal) { 318 | gid = (int) ((UnixNumericGroupPrincipal)principal).longValue(); 319 | } 320 | } 321 | 322 | if (uid != -1) { 323 | try { 324 | Files.setAttribute(target, "unix:uid", uid, NOFOLLOW_LINKS); 325 | } catch (IOException e) { 326 | LOG.warn("Unable to chown file {}: {}", target, e.getMessage()); 327 | } 328 | } else { 329 | LOG.warn("File created without uid: {}", target); 330 | } 331 | if (gid != -1) { 332 | try { 333 | Files.setAttribute(target, "unix:gid", gid, NOFOLLOW_LINKS); 334 | } catch (IOException e) { 335 | LOG.warn("Unable to chown file {}: {}", target, e.getMessage()); 336 | } 337 | } else { 338 | LOG.warn("File created without gid: {}", target); 339 | } 340 | 341 | try { 342 | Files.setAttribute(target, "unix:mode", mode, NOFOLLOW_LINKS); 343 | } catch (IOException e) { 344 | LOG.warn("Unable to set mode of file {}: {}", target, e.getMessage()); 345 | } 346 | } 347 | 348 | @Override 349 | public boolean move(Inode src, String oldName, Inode dest, String newName) throws IOException { 350 | //TODO - several issues 351 | //1. we might not deal with "." and ".." properly 352 | //2. we might accidentally allow composite paths here ("/dome/dir/down") 353 | //3. we return true (changed) even though in theory a file might be renamed to itself? 354 | long currentParentInodeNumber = getInodeNumber(src); 355 | Path currentParentPath = resolveInode(currentParentInodeNumber); 356 | long destParentInodeNumber = getInodeNumber(dest); 357 | Path destPath = resolveInode(destParentInodeNumber); 358 | Path currentPath = currentParentPath.resolve(oldName); 359 | long targetInodeNumber = resolvePath(currentPath); 360 | Path newPath = destPath.resolve(newName); 361 | try { 362 | Files.move(currentPath, newPath, StandardCopyOption.ATOMIC_MOVE); 363 | } catch (FileAlreadyExistsException e) { 364 | throw new ExistException("path " + newPath); 365 | } 366 | remap(targetInodeNumber, currentPath, newPath); 367 | return true; 368 | } 369 | 370 | @Override 371 | public Inode parentOf(Inode inode) throws IOException { 372 | long inodeNumber = getInodeNumber(inode); 373 | if (inodeNumber == 1) { 374 | throw new NoEntException("no parent"); //its the root 375 | } 376 | Path path = resolveInode(inodeNumber); 377 | Path parentPath = path.getParent(); 378 | long parentInodeNumber = resolvePath(parentPath); 379 | return toFh(parentInodeNumber); 380 | } 381 | 382 | @Override 383 | public int read(Inode inode, byte[] data, long offset, int count) throws IOException { 384 | long inodeNumber = getInodeNumber(inode); 385 | Path path = resolveInode(inodeNumber); 386 | ByteBuffer destBuffer = ByteBuffer.wrap(data, 0, count); 387 | try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) { 388 | return channel.read(destBuffer, offset); 389 | } 390 | } 391 | 392 | @Override 393 | public String readlink(Inode inode) throws IOException { 394 | long inodeNumber = getInodeNumber(inode); 395 | Path path = resolveInode(inodeNumber); 396 | return Files.readSymbolicLink(path).toString(); 397 | } 398 | 399 | @Override 400 | public void remove(Inode parent, String path) throws IOException { 401 | long parentInodeNumber = getInodeNumber(parent); 402 | Path parentPath = resolveInode(parentInodeNumber); 403 | Path targetPath = parentPath.resolve(path); 404 | long targetInodeNumber = resolvePath(targetPath); 405 | try { 406 | Files.delete(targetPath); 407 | } catch (DirectoryNotEmptyException e) { 408 | throw new NotEmptyException("dir " + targetPath + " is note empty", e); 409 | } 410 | unmap(targetInodeNumber, targetPath); 411 | } 412 | 413 | @Override 414 | public Inode symlink(Inode parent, String linkName, String targetName, Subject subject, int mode) throws IOException { 415 | long parentInodeNumber = getInodeNumber(parent); 416 | Path parentPath = resolveInode(parentInodeNumber); 417 | Path link = parentPath.resolve(linkName); 418 | Path target = parentPath.resolve(targetName); 419 | if (!targetName.startsWith("/")) { 420 | target = parentPath.relativize(target); 421 | } 422 | try { 423 | Files.createSymbolicLink(link, target); 424 | } catch (UnsupportedOperationException e) { 425 | throw new NotSuppException("Not supported", e); 426 | } catch (FileAlreadyExistsException e) { 427 | throw new ExistException("Path exists " + linkName, e); 428 | } catch (SecurityException e) { 429 | throw new PermException("Permission denied: " + e.getMessage(), e); 430 | } catch (IOException e) { 431 | throw new ServerFaultException("Failed to create: " + e.getMessage(), e); 432 | } 433 | 434 | setOwnershipAndMode(link, subject, mode); 435 | 436 | long newInodeNumber = fileId.getAndIncrement(); 437 | map(newInodeNumber, link); 438 | return toFh(newInodeNumber); 439 | } 440 | 441 | @Override 442 | public WriteResult write(Inode inode, byte[] data, long offset, int count, StabilityLevel stabilityLevel) throws IOException { 443 | long inodeNumber = getInodeNumber(inode); 444 | Path path = resolveInode(inodeNumber); 445 | ByteBuffer srcBuffer = ByteBuffer.wrap(data, 0, count); 446 | try (FileChannel channel = FileChannel.open(path, StandardOpenOption.WRITE)) { 447 | int bytesWritten = channel.write(srcBuffer, offset); 448 | return new WriteResult(StabilityLevel.FILE_SYNC, bytesWritten); 449 | } 450 | } 451 | 452 | @Override 453 | public void commit(Inode inode, long l, int i) throws IOException { 454 | throw new UnsupportedOperationException("Not supported yet."); 455 | } 456 | 457 | private Stat statPath(Path p, long inodeNumber) throws IOException { 458 | 459 | Class attributeClass = 460 | IS_UNIX ? PosixFileAttributeView.class : DosFileAttributeView.class; 461 | 462 | BasicFileAttributes attrs = Files.getFileAttributeView(p, attributeClass, NOFOLLOW_LINKS).readAttributes(); 463 | 464 | Stat stat = new Stat(); 465 | 466 | stat.setATime(attrs.lastAccessTime().toMillis()); 467 | stat.setCTime(attrs.creationTime().toMillis()); 468 | stat.setMTime(attrs.lastModifiedTime().toMillis()); 469 | 470 | if (IS_UNIX) { 471 | stat.setGid((Integer) Files.getAttribute(p, "unix:gid", NOFOLLOW_LINKS)); 472 | stat.setUid((Integer) Files.getAttribute(p, "unix:uid", NOFOLLOW_LINKS)); 473 | stat.setMode((Integer) Files.getAttribute(p, "unix:mode", NOFOLLOW_LINKS)); 474 | stat.setNlink((Integer) Files.getAttribute(p, "unix:nlink", NOFOLLOW_LINKS)); 475 | } else { 476 | DosFileAttributes dosAttrs = (DosFileAttributes)attrs; 477 | stat.setGid(0); 478 | stat.setUid(0); 479 | int type = dosAttrs.isSymbolicLink() ? Stat.S_IFLNK : dosAttrs.isDirectory() ? Stat.S_IFDIR : Stat.S_IFREG; 480 | stat.setMode( type |(dosAttrs.isReadOnly()? 0400 : 0600)); 481 | stat.setNlink(1); 482 | } 483 | 484 | stat.setDev(17); 485 | stat.setIno(inodeNumber); 486 | stat.setRdev(17); 487 | stat.setSize(attrs.size()); 488 | stat.setGeneration(attrs.lastModifiedTime().toMillis()); 489 | 490 | return stat; 491 | } 492 | 493 | @Override 494 | public int access(Subject subject, Inode inode, int mode) throws IOException { 495 | return mode; 496 | } 497 | 498 | @Override 499 | public Stat getattr(Inode inode) throws IOException { 500 | long inodeNumber = getInodeNumber(inode); 501 | Path path = resolveInode(inodeNumber); 502 | return statPath(path, inodeNumber); 503 | } 504 | 505 | @Override 506 | public void setattr(Inode inode, Stat stat) throws IOException { 507 | if (!IS_UNIX) { 508 | // FIXME: windows must support some kind of attribute update as well 509 | return; 510 | } 511 | 512 | long inodeNumber = getInodeNumber(inode); 513 | Path path = resolveInode(inodeNumber); 514 | PosixFileAttributeView attributeView = Files.getFileAttributeView(path, PosixFileAttributeView.class, NOFOLLOW_LINKS); 515 | if (stat.isDefined(Stat.StatAttribute.OWNER)) { 516 | try { 517 | String uid = String.valueOf(stat.getUid()); 518 | UserPrincipal user = _lookupService.lookupPrincipalByName(uid); 519 | attributeView.setOwner(user); 520 | } catch (IOException e) { 521 | throw new UnsupportedOperationException("set uid failed: " + e.getMessage(), e); 522 | } 523 | } 524 | if (stat.isDefined(Stat.StatAttribute.GROUP)) { 525 | try { 526 | String gid = String.valueOf(stat.getGid()); 527 | GroupPrincipal group = _lookupService.lookupPrincipalByGroupName(gid); 528 | attributeView.setGroup(group); 529 | } catch (IOException e) { 530 | throw new UnsupportedOperationException("set gid failed: " + e.getMessage(), e); 531 | } 532 | } 533 | if (stat.isDefined(Stat.StatAttribute.MODE)) { 534 | try { 535 | Files.setAttribute(path, "unix:mode", stat.getMode(), NOFOLLOW_LINKS); 536 | } catch (IOException e) { 537 | throw new UnsupportedOperationException("set mode unsupported: " + e.getMessage(), e); 538 | } 539 | } 540 | if (stat.isDefined(Stat.StatAttribute.SIZE)) { 541 | try (RandomAccessFile raf = new RandomAccessFile(path.toFile(), "rw")) { 542 | raf.setLength(stat.getSize()); 543 | } 544 | } 545 | if (stat.isDefined(Stat.StatAttribute.ATIME)) { 546 | try { 547 | FileTime time = FileTime.fromMillis(stat.getCTime()); 548 | Files.setAttribute(path, "unix:lastAccessTime", time, NOFOLLOW_LINKS); 549 | } catch (IOException e) { 550 | throw new UnsupportedOperationException("set atime failed: " + e.getMessage(), e); 551 | } 552 | } 553 | if (stat.isDefined(Stat.StatAttribute.MTIME)) { 554 | try { 555 | FileTime time = FileTime.fromMillis(stat.getMTime()); 556 | Files.setAttribute(path, "unix:lastModifiedTime", time, NOFOLLOW_LINKS); 557 | } catch (IOException e) { 558 | throw new UnsupportedOperationException("set mtime failed: " + e.getMessage(), e); 559 | } 560 | } 561 | if (stat.isDefined(Stat.StatAttribute.CTIME)) { 562 | try { 563 | FileTime time = FileTime.fromMillis(stat.getCTime()); 564 | Files.setAttribute(path, "unix:ctime", time, NOFOLLOW_LINKS); 565 | } catch (IOException e) { 566 | throw new UnsupportedOperationException("set ctime failed: " + e.getMessage(), e); 567 | } 568 | } 569 | } 570 | 571 | @Override 572 | public nfsace4[] getAcl(Inode inode) throws IOException { 573 | return new nfsace4[0]; 574 | } 575 | 576 | @Override 577 | public void setAcl(Inode inode, nfsace4[] acl) throws IOException { 578 | // NOP 579 | } 580 | 581 | @Override 582 | public boolean hasIOLayout(Inode inode) throws IOException { 583 | return false; 584 | } 585 | 586 | @Override 587 | public AclCheckable getAclCheckable() { 588 | return AclCheckable.UNDEFINED_ALL; 589 | } 590 | 591 | @Override 592 | public NfsIdMapping getIdMapper() { 593 | return _idMapper; 594 | } 595 | 596 | @Override 597 | public boolean getCaseInsensitive() { 598 | return true; 599 | } 600 | 601 | @Override 602 | public boolean getCasePreserving() { 603 | return true; 604 | } 605 | 606 | } 607 | -------------------------------------------------------------------------------- /src/main/java/org/dcache/simplenfs/SimpleNfsServer.java: -------------------------------------------------------------------------------- 1 | package org.dcache.simplenfs; 2 | 3 | import org.dcache.nfs.ExportFile; 4 | import org.dcache.nfs.v3.MountServer; 5 | import org.dcache.nfs.v3.NfsServerV3; 6 | import org.dcache.nfs.v3.xdr.mount_prot; 7 | import org.dcache.nfs.v3.xdr.nfs3_prot; 8 | import org.dcache.nfs.v4.MDSOperationExecutor; 9 | import org.dcache.nfs.v4.NFSServerV41; 10 | import org.dcache.nfs.v4.xdr.nfs4_prot; 11 | import org.dcache.nfs.vfs.VirtualFileSystem; 12 | import org.dcache.oncrpc4j.rpc.OncRpcProgram; 13 | import org.dcache.oncrpc4j.rpc.OncRpcSvc; 14 | import org.dcache.oncrpc4j.rpc.OncRpcSvcBuilder; 15 | 16 | import java.io.Closeable; 17 | import java.io.IOException; 18 | import java.io.InputStreamReader; 19 | import java.io.UncheckedIOException; 20 | import java.nio.file.Files; 21 | import java.nio.file.Path; 22 | import java.util.stream.Collectors; 23 | 24 | public class SimpleNfsServer implements Closeable { 25 | private final OncRpcSvc nfsSvc; 26 | private final Path root; 27 | private final int port; 28 | private final String name; 29 | 30 | public SimpleNfsServer(Path root) { 31 | this(0, 2049, root, null, null); 32 | } 33 | 34 | public SimpleNfsServer(int nfsVers, int port, Path root, ExportFile exportFile, String name) { 35 | try { 36 | NfsServerV3 nfs3 = null; 37 | NFSServerV41 nfs4 = null; 38 | boolean startNfsV3 = ((nfsVers == 0) || (nfsVers == 3)); 39 | boolean startNfsV4 = ((nfsVers == 0) || (nfsVers == 4)); 40 | 41 | if (exportFile == null) { 42 | exportFile = new ExportFile(new InputStreamReader(SimpleNfsServer.class.getClassLoader().getResourceAsStream("exports"))); 43 | } 44 | 45 | this.port = port; 46 | 47 | if (root == null) { 48 | root = Files.createTempDirectory(null); 49 | } 50 | this.root = root; 51 | 52 | if (name == null) { 53 | name = "nfs@" + this.port; 54 | } 55 | this.name = name; 56 | 57 | VirtualFileSystem vfs = new LocalFileSystem(this.root, exportFile.exports().collect(Collectors.toList())); 58 | 59 | nfsSvc = new OncRpcSvcBuilder() 60 | .withPort(this.port) 61 | .withTCP() 62 | .withAutoPublish() 63 | .withWorkerThreadIoStrategy() 64 | .withServiceName(this.name) 65 | .build(); 66 | 67 | if (startNfsV4) { 68 | nfs4 = new NFSServerV41.Builder() 69 | .withVfs(vfs) 70 | .withOperationExecutor(new MDSOperationExecutor()) 71 | .withExportTable(exportFile) 72 | .build(); 73 | } 74 | 75 | if (startNfsV3) { 76 | nfs3 = new NfsServerV3(exportFile, vfs); 77 | } 78 | 79 | MountServer mountd = new MountServer(exportFile, vfs); 80 | 81 | if (startNfsV3) { 82 | nfsSvc.register(new OncRpcProgram(mount_prot.MOUNT_PROGRAM, mount_prot.MOUNT_V3), mountd); 83 | nfsSvc.register(new OncRpcProgram(mount_prot.MOUNT_PROGRAM, mount_prot.MOUNT_V1), mountd); 84 | nfsSvc.register(new OncRpcProgram(nfs3_prot.NFS_PROGRAM, nfs3_prot.NFS_V3), nfs3); 85 | } 86 | 87 | if (startNfsV4) { 88 | nfsSvc.register(new OncRpcProgram(nfs4_prot.NFS4_PROGRAM, nfs4_prot.NFS_V4), nfs4); 89 | } 90 | 91 | nfsSvc.start(); 92 | } catch (IOException e) { 93 | throw new UncheckedIOException(e); 94 | } 95 | } 96 | 97 | @Override 98 | public void close() throws IOException { 99 | nfsSvc.stop(); 100 | } 101 | 102 | public Path getRoot() { 103 | return root; 104 | } 105 | 106 | public int getPort() { 107 | return port; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/resources/exports: -------------------------------------------------------------------------------- 1 | / *(rw,no_root_squash) -------------------------------------------------------------------------------- /src/main/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%X{mdc.client}] [%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss.SSS} [%X{mdc.client}] [%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | --------------------------------------------------------------------------------