001 /**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018 package org.apache.hadoop.fs;
019
020 import java.io.IOException;
021 import java.util.LinkedList;
022 import java.util.regex.Matcher;
023 import java.util.regex.Pattern;
024
025 import org.apache.commons.logging.Log;
026 import org.apache.hadoop.classification.InterfaceAudience;
027 import org.apache.hadoop.classification.InterfaceStability;
028 import org.apache.hadoop.fs.permission.ChmodParser;
029 import org.apache.hadoop.fs.permission.FsPermission;
030 import org.apache.hadoop.fs.shell.CommandFactory;
031 import org.apache.hadoop.fs.shell.CommandFormat;
032 import org.apache.hadoop.fs.shell.FsCommand;
033 import org.apache.hadoop.fs.shell.PathData;
034 import org.apache.hadoop.util.Shell;
035
036 /**
037 * This class is the home for file permissions related commands.
038 * Moved to this separate class since FsShell is getting too large.
039 */
040 @InterfaceAudience.Private
041 @InterfaceStability.Unstable
042 public class FsShellPermissions extends FsCommand {
043
044 static Log LOG = FsShell.LOG;
045
046 /**
047 * Register the permission related commands with the factory
048 * @param factory the command factory
049 */
050 public static void registerCommands(CommandFactory factory) {
051 factory.addClass(Chmod.class, "-chmod");
052 factory.addClass(Chown.class, "-chown");
053 factory.addClass(Chgrp.class, "-chgrp");
054 }
055
056 /**
057 * The pattern is almost as flexible as mode allowed by chmod shell command.
058 * The main restriction is that we recognize only rwxXt. To reduce errors we
059 * also enforce octal mode specifications of either 3 digits without a sticky
060 * bit setting or four digits with a sticky bit setting.
061 */
062 public static class Chmod extends FsShellPermissions {
063 public static final String NAME = "chmod";
064 public static final String USAGE = "[-R] <MODE[,MODE]... | OCTALMODE> PATH...";
065 public static final String DESCRIPTION =
066 "Changes permissions of a file.\n" +
067 "\tThis works similar to shell's chmod with a few exceptions.\n\n" +
068 "-R\tmodifies the files recursively. This is the only option\n" +
069 "\tcurrently supported.\n\n" +
070 "MODE\tMode is same as mode used for chmod shell command.\n" +
071 "\tOnly letters recognized are 'rwxXt'. E.g. +t,a+r,g-w,+rwx,o=r\n\n" +
072 "OCTALMODE Mode specifed in 3 or 4 digits. If 4 digits, the first may\n" +
073 "be 1 or 0 to turn the sticky bit on or off, respectively. Unlike " +
074 "shell command, it is not possible to specify only part of the mode\n" +
075 "\tE.g. 754 is same as u=rwx,g=rx,o=r\n\n" +
076 "\tIf none of 'augo' is specified, 'a' is assumed and unlike\n" +
077 "\tshell command, no umask is applied.";
078
079 protected ChmodParser pp;
080
081 @Override
082 protected void processOptions(LinkedList<String> args) throws IOException {
083 CommandFormat cf = new CommandFormat(2, Integer.MAX_VALUE, "R", null);
084 cf.parse(args);
085 setRecursive(cf.getOpt("R"));
086
087 String modeStr = args.removeFirst();
088 try {
089 pp = new ChmodParser(modeStr);
090 } catch (IllegalArgumentException iea) {
091 // TODO: remove "chmod : " so it's not doubled up in output, but it's
092 // here for backwards compatibility...
093 throw new IllegalArgumentException(
094 "chmod : mode '" + modeStr + "' does not match the expected pattern.");
095 }
096 }
097
098 @Override
099 protected void processPath(PathData item) throws IOException {
100 short newperms = pp.applyNewPermission(item.stat);
101 if (item.stat.getPermission().toShort() != newperms) {
102 try {
103 item.fs.setPermission(item.path, new FsPermission(newperms));
104 } catch (IOException e) {
105 LOG.debug("Error changing permissions of " + item, e);
106 throw new IOException(
107 "changing permissions of '" + item + "': " + e.getMessage());
108 }
109 }
110 }
111 }
112
113 // used by chown/chgrp
114 static private String allowedChars = Shell.WINDOWS ? "[-_./@a-zA-Z0-9 ]" :
115 "[-_./@a-zA-Z0-9]";
116
117 /**
118 * Used to change owner and/or group of files
119 */
120 public static class Chown extends FsShellPermissions {
121 public static final String NAME = "chown";
122 public static final String USAGE = "[-R] [OWNER][:[GROUP]] PATH...";
123 public static final String DESCRIPTION =
124 "Changes owner and group of a file.\n" +
125 "\tThis is similar to shell's chown with a few exceptions.\n\n" +
126 "\t-R\tmodifies the files recursively. This is the only option\n" +
127 "\tcurrently supported.\n\n" +
128 "\tIf only owner or group is specified then only owner or\n" +
129 "\tgroup is modified.\n\n" +
130 "\tThe owner and group names may only consist of digits, alphabet,\n"+
131 "\tand any of " + allowedChars + ". The names are case sensitive.\n\n" +
132 "\tWARNING: Avoid using '.' to separate user name and group though\n" +
133 "\tLinux allows it. If user names have dots in them and you are\n" +
134 "\tusing local file system, you might see surprising results since\n" +
135 "\tshell command 'chown' is used for local files.";
136
137 ///allows only "allowedChars" above in names for owner and group
138 static private final Pattern chownPattern = Pattern.compile(
139 "^\\s*(" + allowedChars + "+)?([:](" + allowedChars + "*))?\\s*$");
140
141 protected String owner = null;
142 protected String group = null;
143
144 @Override
145 protected void processOptions(LinkedList<String> args) throws IOException {
146 CommandFormat cf = new CommandFormat(2, Integer.MAX_VALUE, "R");
147 cf.parse(args);
148 setRecursive(cf.getOpt("R"));
149 parseOwnerGroup(args.removeFirst());
150 }
151
152 /**
153 * Parse the first argument into an owner and group
154 * @param ownerStr string describing new ownership
155 */
156 protected void parseOwnerGroup(String ownerStr) {
157 Matcher matcher = chownPattern.matcher(ownerStr);
158 if (!matcher.matches()) {
159 throw new IllegalArgumentException(
160 "'" + ownerStr + "' does not match expected pattern for [owner][:group].");
161 }
162 owner = matcher.group(1);
163 group = matcher.group(3);
164 if (group != null && group.length() == 0) {
165 group = null;
166 }
167 if (owner == null && group == null) {
168 throw new IllegalArgumentException(
169 "'" + ownerStr + "' does not specify owner or group.");
170 }
171 }
172
173 @Override
174 protected void processPath(PathData item) throws IOException {
175 //Should we do case insensitive match?
176 String newOwner = (owner == null || owner.equals(item.stat.getOwner())) ?
177 null : owner;
178 String newGroup = (group == null || group.equals(item.stat.getGroup())) ?
179 null : group;
180
181 if (newOwner != null || newGroup != null) {
182 try {
183 item.fs.setOwner(item.path, newOwner, newGroup);
184 } catch (IOException e) {
185 LOG.debug("Error changing ownership of " + item, e);
186 throw new IOException(
187 "changing ownership of '" + item + "': " + e.getMessage());
188 }
189 }
190 }
191 }
192
193 /**
194 * Used to change group of files
195 */
196 public static class Chgrp extends Chown {
197 public static final String NAME = "chgrp";
198 public static final String USAGE = "[-R] GROUP PATH...";
199 public static final String DESCRIPTION =
200 "This is equivalent to -chown ... :GROUP ...";
201
202 static private final Pattern chgrpPattern =
203 Pattern.compile("^\\s*(" + allowedChars + "+)\\s*$");
204
205 @Override
206 protected void parseOwnerGroup(String groupStr) {
207 Matcher matcher = chgrpPattern.matcher(groupStr);
208 if (!matcher.matches()) {
209 throw new IllegalArgumentException(
210 "'" + groupStr + "' does not match expected pattern for group");
211 }
212 owner = null;
213 group = matcher.group(1);
214 }
215 }
216 }