synchronize-shares [plain text]
ENV.clear
ENV['__CF_USER_TEXT_ENCODING'] = "0x#{Process::Sys::getuid()}:0:0"
require 'osx/foundation';
require 'optparse';
require 'open3'
ENV.clear
$0 = File.basename($0)
$GUEST = false
$VERBOSE = false
$READONLY = false
begin
mutex = File.open('/var/samba/shares.mutex', 'r')
if (!mutex.flock(File::LOCK_EX))
$READONLY = true
end
rescue
$READONLY = true
end
class ShellCommand
DSCL = '/usr/bin/dscl'
NET = '/usr/bin/net'
def ShellCommand.run(*cmd)
return false unless cmd.length > 1
return false unless (cmd[0] == DSCL || cmd[0] == NET)
print "#{$0}: running command: '#{cmd.join("' '")}'\n" if $VERBOSE;
io = Open3.popen3(*cmd) { | stdin, stdout, stderr |
stdin.close
loop do
read_array = [stdout, stderr].reject { |fd| fd.closed? }
break if read_array.empty?
ready = Kernel.select(read_array, nil, nil, 0.1)
next if ready == nil
ready.flatten.each { | fd |
begin
line = fd.readline
rescue
fd.close
next
end
if (fd == stderr)
$stderr.print "#{$0} (#{cmd[0]}): #{line}" \
if $VERBOSE
else
if block_given?
yield line
else
$stdout.print line \
if $VERBOSE
end
end
}
end
}
end
end
class NotificationCenter
def initialize
@center = OSX::NSDistributedNotificationCenter.defaultCenter()
end
def post(notification, info)
@center.postNotificationName_object_userInfo_options(
notification, nil, info, OSX::NSNotificationPostToAllSessions);
end
end
class SharePoints
def smbconf(sharename)
return nil unless @shares.has_key?(sharename)
strval = "[#{sharename}]\n"
@shares[sharename].keys.sort.each { | key |
confstr = UserShares.mapattr(key, @shares[sharename][key])
next unless confstr
strval += "\t#{confstr}\n"
}
return strval
end
def initialize
@shares = {} share = {}
data = ''
ShellCommand.run(ShellCommand::DSCL, '-plist',
'.', '-readall', '/SharePoints') { |line|
data += line
}
cfdata = OSX::NSData.dataWithBytes_length(data, data.length)
plist, format, err = \
OSX::NSPropertyListSerialization.propertyListFromData_mutabilityOption_format_errorDescription(cfdata,
OSX::NSPropertyListImmutable)
if (plist == nil or !plist.kind_of? OSX::NSCFArray)
$stderr.print "#{$0}: failed to parse dscl plist\n" \
if $VERBOSE
return nil
end
plist.each { | entry |
s = SharePoints.to_native(entry)
if s.has_key?('dsAttrTypeNative:smb_name')
name = s['dsAttrTypeNative:smb_name']
@shares[name] = s
else
$stderr.print "#{$0}: ignoring share with missing smb_name\n" \
if $VERBOSE
end
}
end
def each_key
@shares.each_key { | key |
yield key
}
end
def each
@shares.each { | key, hash |
yield key, hash
}
end
private
def SharePoints.to_native(val)
return nil if val == nil
if val.kind_of? OSX::NSCFBoolean
return (val == OSX::KCFBooleanTrue ? true : false)
end
if val.kind_of? OSX::NSCFString
return val.to_s
end
if val.kind_of? OSX::NSCFNumber
return val.to_i
end
if val.kind_of? OSX::NSCFArray
array = []
val.each { |element| array += [ SharePoints.to_native(element) ] }
case array.length
when 0
return nil
when 1
return array[0]
else
return array
end
end
if val.kind_of? OSX::NSCFDictionary
hash = {}
val.allKeys().each { | key |
new_key = SharePoints.to_native(key)
new_val = SharePoints.to_native(val[key])
hash[new_key] = new_val
}
return hash
end
$stderr.print \
"#{$0}: preferences type #{val.class} is not supported\n" \
if $VERBOSE
return nil
end
end
class UserShares
ATTRIBUTE_MAP =
{
'dsAttrTypeNative:directory_path' => 'path',
'dsAttrTypeNative:name' => 'comment',
'dsAttrTypeNative:smb_guestaccess' => 'guest ok',
'dsAttrTypeNative:smb_inherit_permissions' => 'inherit permissions',
'dsAttrTypeNative:smb_createmask' => 'create mask',
'dsAttrTypeNative:smb_directorymask' => 'directory mask',
'dsAttrTypeNative:smb_oplocks' => 'oplocks',
'dsAttrTypeNative:smb_strictlocking' => 'strict locking',
}
def UserShares.mapattr(key, value)
return nil unless ATTRIBUTE_MAP.has_key?(key)
case value
when '1'
val = 'yes'
when '0'
val = 'no'
else
val = value
end
return "#{ATTRIBUTE_MAP[key]}=#{val}"
end
def initialize
@shares = []
ShellCommand.run(ShellCommand::NET,
'usershare', 'list', '--long') { | line |
if line =~ /^\s*(.+)\s*$/
sharename = $1
@shares.push(sharename)
end
}
@shares = @shares.sort
end
def smbconf(sharename)
return nil unless @shares.include?(sharename)
strval = ""
ShellCommand.run(ShellCommand::NET,
'usershare', 'info', '--long', sharename) { |line|
if line =~ /\[.+\]/
strval += "#{line}"
else
line = line.sub(/([^[:space:]])=([^[:space:]])/, '\1 = \2')
strval += "\t#{line}"
end
}
return strval
end
def each
@shares.each { | sharename | yield sharename }
end
def validate(sharehash)
invalid = Regexp.new('[%<>*?|\/\\+=;:\$",]')
return false unless (
sharehash.has_key?('dsAttrTypeNative:smb_shared') &&
sharehash.has_key?('dsAttrTypeNative:smb_name') &&
sharehash.has_key?('dsAttrTypeNative:directory_path')
)
return false if (sharehash['dsAttrTypeNative:smb_name'] =~ invalid)
return true
end
def clear
self.each { | sharename |
ShellCommand.run(ShellCommand::NET, 'usershare', 'delete', sharename);
}
end
def store(sharename, sharehash)
unless validate(sharehash)
print "#{$0}: invalid share [#{sharename}] \n" if $VERBOSE
return false
end
if sharehash['dsAttrTypeNative:smb_shared'] != '1'
print "#{$0}: share [#{sharename}] is disabled\n" if $VERBOSE
return
end
path = sharehash['dsAttrTypeNative:directory_path']
name = sharehash['dsAttrTypeNative:smb_name']
comment = sharehash['dsAttrTypeNative:smb_name']
if name != sharename
raise ArgumentError, "inconsistent share #{sharename}", caller
end
acl = 'S-1-1-0:F'
if sharehash.has_key? 'dsAttrTypeNative:smb_guestaccess'
if sharehash['dsAttrTypeNative:smb_guestaccess'] == '1'
guest = 'guest_ok=y'
else
guest = 'guest_ok=n'
end
else
guest = $GUEST ? 'guest_ok=y' : 'guest_ok=n'
end
args = [ 'usershare', 'add', name, path, comment, acl, guest]
sharehash.each { |key, value|
case key
when 'dsAttrTypeNative:directory_path'
next
when 'dsAttrTypeNative:name'
next
when 'dsAttrTypeNative:smb_guestaccess'
next
end
argval = UserShares.mapattr(key, value)
next unless argval
args.push(argval) if argval
}
ShellCommand.run(ShellCommand::NET, *args)
return true
end
end
opts = OptionParser.new
opts.on('--verbose',
'print extra debugging messages') {
$VERBOSE = true
}
opts.on('--enable-guest',
'enable guest access by default') {
$GUEST = true
}
opts.on('--list-current',
'print the current share configuration') {
ushares = UserShares.new()
ushares.each { | sharename |
$stdout.print ushares.smbconf(sharename)
}
exit 0
}
opts.on('--list-pending',
'print the pending share configuration') {
dshares = SharePoints.new()
dshares.each_key { | sharename |
$stdout.print dshares.smbconf(sharename)
}
exit 0
}
begin
opts.parse!(ARGV) if (ARGV.length != 0)
raise OptionParser::InvalidOption, ARGV[0], caller
end
rescue OptionParser::InvalidOption => err
$stderr.print "#{$0}: #{err}\n"
$stderr.print opts.help()
exit 1
end
if $READONLY
exit 1
end
dshares = SharePoints.new()
ushares = UserShares.new()
ushares.clear()
dshares.each { | sharename, sharehash |
ushares.store(sharename, sharehash)
}
notify = NotificationCenter.new()
notify.post('com.apple.ServiceConfigurationChangedNotification',
{ 'ServiceName' => 'sharepoints'});