win32-file 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/win32/file.rb ADDED
@@ -0,0 +1,1037 @@
1
+ require 'windows/security'
2
+ require 'windows/limits'
3
+ require 'win32/file/stat'
4
+
5
+ class File
6
+ # Some of these are courtesy of win32-file-stat
7
+ include Windows::Error
8
+ include Windows::File
9
+ include Windows::Security
10
+ include Windows::Limits
11
+ include Windows::DeviceIO
12
+ extend Windows::Error
13
+ extend Windows::File
14
+ extend Windows::Path
15
+ extend Windows::Security
16
+ extend Windows::MSVCRT::Buffer
17
+
18
+ VERSION = '0.5.3'
19
+ MAX_PATH = 260
20
+
21
+ # Abbreviated attribute constants for convenience
22
+ ARCHIVE = FILE_ATTRIBUTE_ARCHIVE
23
+ COMPRESSED = FILE_ATTRIBUTE_COMPRESSED
24
+ HIDDEN = FILE_ATTRIBUTE_HIDDEN
25
+ NORMAL = FILE_ATTRIBUTE_NORMAL
26
+ OFFLINE = FILE_ATTRIBUTE_OFFLINE
27
+ READONLY = FILE_ATTRIBUTE_READONLY
28
+ SYSTEM = FILE_ATTRIBUTE_SYSTEM
29
+ TEMPORARY = FILE_ATTRIBUTE_TEMPORARY
30
+ INDEXED = 0x0002000
31
+ CONTENT_INDEXED = 0x0002000
32
+
33
+ # Custom Security rights
34
+ FULL = STANDARD_RIGHTS_ALL | FILE_READ_DATA | FILE_WRITE_DATA |
35
+ FILE_APPEND_DATA | FILE_READ_EA | FILE_WRITE_EA | FILE_EXECUTE |
36
+ FILE_DELETE_CHILD | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES
37
+
38
+ CHANGE = FILE_GENERIC_WRITE | FILE_GENERIC_READ | FILE_EXECUTE | DELETE
39
+ READ = FILE_GENERIC_READ | FILE_EXECUTE
40
+ ADD = 0x001201bf
41
+
42
+ SECURITY_RIGHTS = {
43
+ 'FULL' => FULL,
44
+ 'DELETE' => DELETE,
45
+ 'READ' => READ,
46
+ 'CHANGE' => CHANGE,
47
+ 'ADD' => ADD
48
+ }
49
+
50
+ ### Class Methods
51
+
52
+ ## Security
53
+
54
+ # Sets the file permissions for the given file name. The 'permissions'
55
+ # argument is a hash with an account name as the key, and the various
56
+ # permission constants as possible values. The possible constant values
57
+ # are:
58
+ #
59
+ # FILE_READ_DATA
60
+ # FILE_WRITE_DATA
61
+ # FILE_APPEND_DATA
62
+ # FILE_READ_EA
63
+ # FILE_WRITE_EA
64
+ # FILE_EXECUTE
65
+ # FILE_DELETE_CHILD
66
+ # FILE_READ_ATTRIBUTES
67
+ # FILE_WRITE_ATTRIBUTES
68
+ # STANDARD_RIGHTS_ALL
69
+ # FULL
70
+ # READ
71
+ # ADD
72
+ # CHANGE
73
+ # DELETE
74
+ # READ_CONTROL
75
+ # WRITE_DAC
76
+ # WRITE_OWNER
77
+ # SYNCHRONIZE
78
+ # STANDARD_RIGHTS_REQUIRED
79
+ # STANDARD_RIGHTS_READ
80
+ # STANDARD_RIGHTS_WRITE
81
+ # STANDARD_RIGHTS_EXECUTE
82
+ # STANDARD_RIGHTS_ALL
83
+ # SPECIFIC_RIGHTS_ALL
84
+ # ACCESS_SYSTEM_SECURITY
85
+ # MAXIMUM_ALLOWED
86
+ # GENERIC_READ
87
+ # GENERIC_WRITE
88
+ # GENERIC_EXECUTE
89
+ # GENERIC_ALL
90
+ #
91
+ def self.set_permissions(file, perms)
92
+ raise TypeError unless perms.kind_of?(Hash)
93
+
94
+ account_rights = 0
95
+ sec_desc = 0.chr * SECURITY_DESCRIPTOR_MIN_LENGTH
96
+
97
+ unless InitializeSecurityDescriptor(sec_desc, 1)
98
+ raise ArgumentError, get_last_error
99
+ end
100
+
101
+ cb_acl = 1024
102
+ cb_sid = 1024
103
+
104
+ acl_new = 0.chr * cb_acl
105
+
106
+ unless InitializeAcl(acl_new, cb_acl, ACL_REVISION2)
107
+ raise ArgumentError, get_last_error
108
+ end
109
+
110
+ sid = 0.chr * cb_sid
111
+ snu_type = 0.chr * cb_sid
112
+
113
+ all_ace = 0.chr * ALLOW_ACE_LENGTH
114
+ all_ace_ptr = memset(all_ace, 0, 0) # address of all_ace
115
+
116
+ # all_ace_ptr->Header.AceType = ACCESS_ALLOWED_ACE_TYPE
117
+ all_ace[0] = 0
118
+
119
+ perms.each{ |account, mask|
120
+ next if mask.nil?
121
+
122
+ cch_domain = [80].pack('L')
123
+ cb_sid = [1024].pack('L')
124
+ domain_buf = 0.chr * 80
125
+
126
+ server, account = account.split("\\")
127
+
128
+ if ['BUILTIN', 'NT AUTHORITY'].include?(server.upcase)
129
+ server = nil
130
+ end
131
+
132
+ val = LookupAccountName(
133
+ server,
134
+ account,
135
+ sid,
136
+ cb_sid,
137
+ domain_buf,
138
+ cch_domain,
139
+ snu_type
140
+ )
141
+
142
+ if val == 0
143
+ raise ArgumentError, get_last_error
144
+ end
145
+
146
+ size = [0,0,0,0,0].pack('CCSLL').length # sizeof(ACCESS_ALLOWED_ACE)
147
+
148
+ val = CopySid(
149
+ ALLOW_ACE_LENGTH - size,
150
+ all_ace_ptr + 8, # address of all_ace_ptr->SidStart
151
+ sid
152
+ )
153
+
154
+ if val == 0
155
+ raise ArgumentError, get_last_error
156
+ end
157
+
158
+ if (GENERIC_ALL & mask).nonzero?
159
+ account_rights = GENERIC_ALL & mask
160
+ elsif (GENERIC_RIGHTS_CHK & mask).nonzero?
161
+ account_rights = GENERIC_RIGHTS_MASK & mask
162
+ end
163
+
164
+ # all_ace_ptr->Header.AceFlags = INHERIT_ONLY_ACE | OBJECT_INHERIT_ACE;
165
+ all_ace[1] = INHERIT_ONLY_ACE | OBJECT_INHERIT_ACE
166
+
167
+ 2.times{
168
+ if account_rights != 0
169
+ all_ace[2,2] = [12 - 4 + GetLengthSid(sid)].pack('S')
170
+ all_ace[4,4] = [account_rights].pack('L')
171
+
172
+ val = AddAce(
173
+ acl_new,
174
+ ACL_REVISION2,
175
+ MAXDWORD,
176
+ all_ace_ptr,
177
+ all_ace[2,2].unpack('S').first
178
+ )
179
+
180
+ if val == 0
181
+ raise ArgumentError, get_last_error
182
+ end
183
+
184
+ # all_ace_ptr->Header.AceFlags = CONTAINER_INHERIT_ACE
185
+ all_ace[1] = CONTAINER_INHERIT_ACE
186
+ else
187
+ # all_ace_ptr->Header.AceFlags = 0
188
+ all_ace[1] = 0
189
+ end
190
+
191
+ account_rights = REST_RIGHTS_MASK & mask
192
+ }
193
+ }
194
+
195
+ unless SetSecurityDescriptorDacl(sec_desc, 1, acl_new, 0)
196
+ raise ArgumentError, get_last_error
197
+ end
198
+
199
+ unless SetFileSecurity(file, DACL_SECURITY_INFORMATION, sec_desc)
200
+ raise ArgumentError, get_last_error
201
+ end
202
+
203
+ self
204
+ end
205
+
206
+ # Returns an array of human-readable strings that correspond to the
207
+ # permission flags.
208
+ #
209
+ def self.securities(mask)
210
+ sec_array = []
211
+ if mask == 0
212
+ sec_array.push('NONE')
213
+ else
214
+ if (mask & FULL) ^ FULL == 0
215
+ sec_array.push('FULL')
216
+ else
217
+ SECURITY_RIGHTS.each{ |string, numeric|
218
+ if (numeric & mask) ^ numeric == 0
219
+ sec_array.push(string)
220
+ end
221
+ }
222
+ end
223
+ end
224
+ sec_array
225
+ end
226
+
227
+ # Returns a hash describing the current file permissions for the given file.
228
+ # The account name is the key, and the value is an integer representing
229
+ # an or'd value that corresponds to the security permissions for that file.
230
+ #
231
+ # To get a human readable version of the permissions, pass the value to the
232
+ # +File.securities+ method.
233
+ #
234
+ def self.get_permissions(file, host=nil)
235
+ current_length = 0
236
+ length_needed = [0].pack('L')
237
+ sec_buf = ''
238
+
239
+ loop do
240
+ bool = GetFileSecurity(
241
+ file,
242
+ DACL_SECURITY_INFORMATION,
243
+ sec_buf,
244
+ sec_buf.length,
245
+ length_needed
246
+ )
247
+
248
+ if bool == 0 && GetLastError() != ERROR_INSUFFICIENT_BUFFER
249
+ raise ArgumentError, get_last_error
250
+ end
251
+
252
+ break if sec_buf.length >= length_needed.unpack('L').first
253
+ sec_buf += ' ' * length_needed.unpack('L').first
254
+ end
255
+
256
+ control = [0].pack('L')
257
+ revision = [0].pack('L')
258
+
259
+ unless GetSecurityDescriptorControl(sec_buf, control, revision)
260
+ raise ArgumentError, get_last_error
261
+ end
262
+
263
+ # No DACL exists
264
+ if (control.unpack('L').first & SE_DACL_PRESENT) == 0
265
+ raise ArgumentError, 'No DACL present: explicit deny all'
266
+ end
267
+
268
+ dacl_present = [0].pack('L')
269
+ dacl_defaulted = [0].pack('L')
270
+ dacl_ptr = [0].pack('L')
271
+
272
+ val = GetSecurityDescriptorDacl(
273
+ sec_buf,
274
+ dacl_present,
275
+ dacl_ptr,
276
+ dacl_defaulted
277
+ )
278
+
279
+ if val == 0
280
+ raise ArgumentError, get_last_error
281
+ end
282
+
283
+ acl_buf = 0.chr * 8 # byte, byte, word, word, word (struct ACL)
284
+ memcpy(acl_buf, dacl_ptr.unpack('L').first, acl_buf.size)
285
+
286
+ if acl_buf.unpack('CCSSS').first == 0
287
+ raise ArgumentError, 'DACL is NULL: implicit access grant'
288
+ end
289
+
290
+ ace_ptr = [0].pack('L')
291
+ ace_count = acl_buf.unpack('CCSSS')[3]
292
+
293
+ perms_hash = {}
294
+ 0.upto(ace_count - 1){ |i|
295
+ unless GetAce(dacl_ptr.unpack('L').first, i, ace_ptr)
296
+ next
297
+ end
298
+
299
+ ace_buf = 0.chr * 12 # ACE_HEADER, dword, dword (ACCESS_ALLOWED_ACE)
300
+ memcpy(ace_buf, ace_ptr.unpack('L').first, ace_buf.size)
301
+
302
+ if ace_buf.unpack('CCS').first == ACCESS_ALLOWED_ACE_TYPE
303
+ name = 0.chr * MAX_PATH
304
+ name_size = [name.size].pack('L')
305
+ domain = 0.chr * MAX_PATH
306
+ domain_size = [domain.size].pack('L')
307
+ snu_ptr = 0.chr * 4
308
+
309
+ val = LookupAccountSid(
310
+ host,
311
+ ace_ptr.unpack('L').first + 8, # address of ace_ptr->SidStart
312
+ name,
313
+ name_size,
314
+ domain,
315
+ domain_size,
316
+ snu_ptr
317
+ )
318
+
319
+ if val == 0
320
+ raise ArgumentError, get_last_error
321
+ end
322
+
323
+ name = name[0..name_size.unpack('L').first].split(0.chr)[0]
324
+ domain = domain[0..domain_size.unpack('L').first].split(0.chr)[0]
325
+ mask = ace_buf.unpack('LLL')[1]
326
+
327
+ unless domain.nil? || domain.empty?
328
+ name = domain + '\\' + name
329
+ end
330
+
331
+ perms_hash[name] = mask
332
+ end
333
+ }
334
+ perms_hash
335
+ end
336
+
337
+ ## Encryption
338
+
339
+ # Encrypts a file or directory. All data streams in a file are encrypted.
340
+ # All new files created in an encrypted directory are encrypted.
341
+ #
342
+ # The caller must have the FILE_READ_DATA, FILE_WRITE_DATA,
343
+ # FILE_READ_ATTRIBUTES, FILE_WRITE_ATTRIBUTES, and SYNCHRONIZE access
344
+ # rights.
345
+ #
346
+ # Requires exclusive access to the file being encrypted, and will fail if
347
+ # another process is using the file. If the file is compressed, EncryptFile
348
+ # will decompress the file before encrypting it.
349
+ #
350
+ # Windows 2000 or later only.
351
+ #
352
+ def self.encrypt(file)
353
+ unless EncryptFile(file)
354
+ raise ArgumentError, get_last_error
355
+ end
356
+ self
357
+ end
358
+
359
+ # Decrypts an encrypted file or directory.
360
+ #
361
+ # The caller must have the FILE_READ_DATA, FILE_WRITE_DATA,
362
+ # FILE_READ_ATTRIBUTES, FILE_WRITE_ATTRIBUTES, and SYNCHRONIZE access
363
+ # rights.
364
+ #
365
+ # Requires exclusive access to the file being decrypted, and will fail if
366
+ # another process is using the file. If the file is not encrypted an error
367
+ # is NOT raised.
368
+ #
369
+ # Windows 2000 or later only.
370
+ #
371
+ def self.decrypt(file)
372
+ unless DecryptFile(file, 0)
373
+ raise ArgumentError, get_last_error
374
+ end
375
+ self
376
+ end
377
+
378
+ ## Path methods
379
+
380
+ # Returns the last component of the filename given in +filename+. If
381
+ # +suffix+ is given and present at the end of +filename+, it is removed.
382
+ # Any extension can be removed by giving an extension of ".*".
383
+ #
384
+ # This was reimplemented because the current version does not handle UNC
385
+ # paths properly, i.e. it should not return anything less than the root.
386
+ # In all other respects it is identical to the current implementation.
387
+ #
388
+ # File.basename("C:\\foo\\bar.txt") -> "bar.txt"
389
+ # File.basename("C:\\foo\\bar.txt", ".txt") -> "bar"
390
+ # File.basename("\\\\foo\\bar") -> "\\\\foo\\bar"
391
+ #
392
+ def self.basename(file, suffix = nil)
393
+ fpath = false
394
+ file = file.dup # Don't modify original string
395
+
396
+ # We have to convert forward slashes to backslashes for the Windows
397
+ # functions to work properly.
398
+ if file.include?('/')
399
+ file.tr!('/', '\\')
400
+ fpath = true
401
+ end
402
+
403
+ # Return an empty or root path as-is.
404
+ if file.empty? || PathIsRoot(file)
405
+ file.tr!("\\", '/') if fpath
406
+ return file
407
+ end
408
+
409
+ PathStripPath(file) # Gives us the basename
410
+
411
+ if suffix
412
+ if suffix == '.*'
413
+ PathRemoveExtension(file)
414
+ else
415
+ if PathFindExtension(file) == suffix
416
+ PathRemoveExtension(file)
417
+ end
418
+ end
419
+ end
420
+
421
+ file = file.split(0.chr).first
422
+
423
+ # Trim trailing slashes
424
+ while file[-1].chr == "\\"
425
+ file.chop!
426
+ end
427
+
428
+ # Return forward slashes if that's how the path was passed in.
429
+ if fpath
430
+ file.tr!("\\", '/')
431
+ end
432
+
433
+ file
434
+ end
435
+
436
+ # Returns all components of the filename given in +filename+ except the
437
+ # last one.
438
+ #
439
+ # This was reimplemented because the current version does not handle UNC
440
+ # paths properly, i.e. it should not return anything less than the root.
441
+ # In all other respects it is identical to the current implementation.
442
+ #
443
+ # File.dirname("C:\\foo\\bar\\baz.txt") -> "C:\\foo\\bar"
444
+ # File.dirname("\\\\foo\\bar") -> "\\\\foo\\bar"
445
+ #
446
+ def self.dirname(file)
447
+ fpath = false
448
+ file = file.dup
449
+
450
+ if file.include?('/')
451
+ file.tr!('/', "\\")
452
+ fpath = true
453
+ end
454
+
455
+ if PathIsRelative(file)
456
+ return '.'
457
+ end
458
+
459
+ if PathIsRoot(file)
460
+ file.tr!("\\", '/') if fpath
461
+ return file
462
+ end
463
+
464
+ PathRemoveFileSpec(file)
465
+ file = file.split(0.chr).first
466
+ PathRemoveBackslash(file)
467
+
468
+ file.tr!("\\", '/') if fpath
469
+ file
470
+ end
471
+
472
+ # Returns +file+ in long format. For example, if 'SOMEFI~1.TXT'
473
+ # was the argument provided, and the short representation for
474
+ # 'somefile.txt', then this method would return 'somefile.txt'.
475
+ #
476
+ # Note that certain file system optimizations may prevent this method
477
+ # from working as expected. In that case, you will get back the file
478
+ # name in 8.3 format.
479
+ #
480
+ def self.long_path(file)
481
+ buf = 0.chr * MAX_PATH
482
+ if GetLongPathName(file, buf, buf.size) == 0
483
+ raise ArgumentError, get_last_error
484
+ end
485
+ File.basename(buf.split(0.chr).first.strip)
486
+ end
487
+
488
+ # Returns 'file_name' in 8.3 format. For example, 'c:\documentation.doc'
489
+ # would be returned as 'c:\docume~1.doc'.
490
+ #
491
+ def self.short_path(file)
492
+ buf = 0.chr * MAX_PATH
493
+ if GetShortPathName(file, buf, buf.size) == 0
494
+ raise ArgumentError, get_last_error
495
+ end
496
+ File.basename(buf.split(0.chr).first.strip)
497
+ end
498
+
499
+ # Splits the given string into a directory and a file component and returns
500
+ # them in a two element array. This was reimplemented because the
501
+ # current version does not handle UNC paths properly.
502
+ #
503
+ def self.split(file)
504
+ array = []
505
+
506
+ if file.empty? || PathIsRoot(file)
507
+ array.push(file, '')
508
+ else
509
+ array.push(File.dirname(file), File.basename(file))
510
+ end
511
+ array
512
+ end
513
+
514
+ ## Stat methods
515
+
516
+ # Returns a File::Stat object, as defined in the win32-file-stat package.
517
+ #
518
+ def self.stat(file)
519
+ File::Stat.new(file)
520
+ end
521
+
522
+ # Identical to File.stat on Windows.
523
+ #
524
+ def self.lstat(file)
525
+ File::Stat.new(file)
526
+ end
527
+
528
+ # Returns the file system's block size.
529
+ #
530
+ def self.blksize(file)
531
+ File::Stat.new(file).blksize
532
+ end
533
+
534
+ # Returns whether or not +file+ is a block device.
535
+ #
536
+ def self.blockdev?(file)
537
+ File::Stat.new(file).blockdev?
538
+ end
539
+
540
+ # Returns true if the file is a character device. This replaces the current
541
+ # Ruby implementation which always returns false.
542
+ #
543
+ def self.chardev?(file)
544
+ File::Stat.new(file).chardev?
545
+ end
546
+
547
+ # Returns the size of the file in bytes.
548
+ #
549
+ # This was reimplemented because the current version does not handle file
550
+ # sizes greater than 2gb.
551
+ #
552
+ def self.size(file)
553
+ File::Stat.new(file).size
554
+ end
555
+
556
+ ## Attribute methods
557
+
558
+ # Returns true if the file or directory is an archive file. Applications
559
+ # use this attribute to mark files for backup or removal.
560
+ #
561
+ def self.archive?(file)
562
+ File::Stat.new(file).archive?
563
+ end
564
+
565
+ # Returns true if the file or directory is compressed. For a file, this
566
+ # means that all of the data in the file is compressed. For a directory,
567
+ # this means that compression is the default for newly created files and
568
+ # subdirectories.
569
+ #
570
+ def self.compressed?(file)
571
+ File::Stat.new(file).compressed?
572
+ end
573
+
574
+ # Returns true if the file or directory is encrypted. For a file, this
575
+ # means that all data in the file is encrypted. For a directory, this
576
+ # means that encryption is the default for newly created files and
577
+ # subdirectories.
578
+ #
579
+ def self.encrypted?(file)
580
+ File::Stat.new(file).encrypted?
581
+ end
582
+
583
+ # Returns true if the file or directory is hidden. It is not included
584
+ # in an ordinary directory listing.
585
+ #
586
+ def self.hidden?(file)
587
+ File::Stat.new(file).hidden?
588
+ end
589
+
590
+ # Returns true if the file or directory is indexed by the content indexing
591
+ # service.
592
+ #
593
+ def self.indexed?(file)
594
+ File::Stat.new(file).indexed?
595
+ end
596
+
597
+ # Returns true if the file or directory has no other attributes set.
598
+ #
599
+ def self.normal?(file)
600
+ File::Stat.new(file).normal?
601
+ end
602
+
603
+ # Returns true if the data of the file is not immediately available. This
604
+ # attribute indicates that the file data has been physically moved to
605
+ # offline storage. This attribute is used by Remote Storage, the
606
+ # hierarchical storage management software. Applications should not
607
+ # arbitrarily change this attribute.
608
+ #
609
+ def self.offline?(file)
610
+ File::Stat.new(file).offline?
611
+ end
612
+
613
+ # Returns true if The file or directory is read-only. Applications can
614
+ # read the file but cannot write to it or delete it. In the case of a
615
+ # directory, applications cannot delete it.
616
+ #
617
+ def self.readonly?(file)
618
+ File::Stat.new(file).readonly?
619
+ end
620
+
621
+
622
+
623
+ # Returns true if the file or directory has an associated reparse point. A
624
+ # reparse point is a collection of user defined data associated with a file
625
+ # or directory. For more on reparse points, search
626
+ # http://msdn.microsoft.com.
627
+ #
628
+ def self.reparse_point?(file)
629
+ File::Stat.new(file).reparse_point?
630
+ end
631
+
632
+ # Returns true if the file is a sparse file. A sparse file is a file in
633
+ # which much of the data is zeros, typically image files. See
634
+ # http://msdn.microsoft.com for more details.
635
+ #
636
+ def self.sparse?(file)
637
+ File::Stat.new(file).sparse?
638
+ end
639
+
640
+ # Returns true if the file or directory is part of the operating system
641
+ # or is used exclusively by the operating system.
642
+ #
643
+ def self.system?(file)
644
+ File::Stat.new(file).system?
645
+ end
646
+
647
+ # Returns true if the file is being used for temporary storage.
648
+ #
649
+ # File systems avoid writing data back to mass storage if sufficient cache
650
+ # memory is available, because often the application deletes the temporary
651
+ # file shortly after the handle is closed. In that case, the system can
652
+ # entirely avoid writing the data. Otherwise, the data will be written after
653
+ # the handle is closed.
654
+ #
655
+ def self.temporary?(file)
656
+ File::Stat.new(file).temporary?
657
+ end
658
+
659
+ # Returns an array of strings indicating the attributes for that file. The
660
+ # possible values are:
661
+ #
662
+ # archive
663
+ # compressed
664
+ # directory
665
+ # encrypted
666
+ # hidden
667
+ # indexed
668
+ # normal
669
+ # offline
670
+ # readonly
671
+ # reparse_point
672
+ # sparse
673
+ # system
674
+ # temporary
675
+ #
676
+ def self.attributes(file)
677
+ attributes = GetFileAttributes(file)
678
+ arr = []
679
+
680
+ if attributes == INVALID_FILE_ATTRIBUTES
681
+ raise ArgumentError, get_last_error
682
+ end
683
+
684
+ arr.push('archive') if archive?(file)
685
+ arr.push('compressed') if compressed?(file)
686
+ arr.push('directory') if directory?(file)
687
+ arr.push('encrypted') if encrypted?(file)
688
+ arr.push('hidden') if hidden?(file)
689
+ arr.push('indexed') if indexed?(file)
690
+ arr.push('normal') if normal?(file)
691
+ arr.push('offline') if offline?(file)
692
+ arr.push('readonly') if readonly?(file)
693
+ arr.push('reparse_point') if reparse_point?(file)
694
+ arr.push('sparse') if sparse?(file)
695
+ arr.push('system') if system?(file)
696
+ arr.push('temporary') if temporary?(file)
697
+
698
+ arr
699
+ end
700
+
701
+ # Sets the file attributes based on the given (numeric) +flags+. This does
702
+ # not remove existing attributes, it merely adds to them.
703
+ #
704
+ def self.set_attributes(file, flags)
705
+ attributes = GetFileAttributes(file)
706
+
707
+ if attributes == INVALID_FILE_ATTRIBUTES
708
+ raise ArgumentError, get_last_error
709
+ end
710
+
711
+ attributes |= flags
712
+
713
+ if SetFileAttributes(file, attributes) == 0
714
+ raise ArgumentError, get_last_error
715
+ end
716
+
717
+ self
718
+ end
719
+
720
+ # Removes the file attributes based on the given (numeric) +flags+.
721
+ #
722
+ def self.remove_attributes(file, flags)
723
+ attributes = GetFileAttributes(file)
724
+
725
+ if attributes == INVALID_FILE_ATTRIBUTES
726
+ raise ArgumentError, get_last_error
727
+ end
728
+
729
+ attributes &= ~flags
730
+
731
+ if SetFileAttributes(file, attributes) == 0
732
+ raise ArgumentError, get_last_error
733
+ end
734
+
735
+ self
736
+ end
737
+
738
+ # Instance methods
739
+
740
+ def stat
741
+ File::Stat.new(self.path)
742
+ end
743
+
744
+ # Sets whether or not the file is an archive file.
745
+ #
746
+ def archive=(bool)
747
+ attributes = GetFileAttributes(self.path)
748
+
749
+ if attributes == INVALID_FILE_ATTRIBUTES
750
+ raise ArgumentError, get_last_error
751
+ end
752
+
753
+ if bool
754
+ attributes |= FILE_ATTRIBUTE_ARCHIVE;
755
+ else
756
+ attributes &= ~FILE_ATTRIBUTE_ARCHIVE;
757
+ end
758
+
759
+ if SetFileAttributes(self.path, attributes) == 0
760
+ raise ArgumentError, get_last_error
761
+ end
762
+
763
+ self
764
+ end
765
+
766
+ # Sets whether or not the file is a compressed file.
767
+ #
768
+ def compressed=(bool)
769
+ in_buf = bool ? COMPRESSION_FORMAT_DEFAULT : COMPRESSION_FORMAT_NONE
770
+ in_buf = [in_buf].pack('L')
771
+ bytes = [0].pack('L')
772
+
773
+ handle = CreateFile(
774
+ self.path,
775
+ FILE_READ_DATA | FILE_WRITE_DATA,
776
+ FILE_SHARE_READ | FILE_SHARE_WRITE,
777
+ 0,
778
+ OPEN_EXISTING,
779
+ 0,
780
+ 0
781
+ )
782
+
783
+ if handle == INVALID_HANDLE_VALUE
784
+ raise ArgumentError, get_last_error
785
+ end
786
+
787
+ val = DeviceIoControl(
788
+ handle,
789
+ FSCTL_SET_COMPRESSION(),
790
+ in_buf,
791
+ in_buf.length,
792
+ 0,
793
+ 0,
794
+ bytes,
795
+ 0
796
+ )
797
+
798
+ if val == 0
799
+ raise ArgumentError, get_last_error
800
+ end
801
+
802
+ self
803
+ end
804
+
805
+ # Sets the hidden attribute to true or false. Setting this attribute to
806
+ # true means that the file is not included in an ordinary directory listing.
807
+ #
808
+ def hidden=(bool)
809
+ attributes = GetFileAttributes(self.path)
810
+
811
+ if attributes == INVALID_FILE_ATTRIBUTES
812
+ raise ArgumentError, get_last_error
813
+ end
814
+
815
+ if bool
816
+ attributes |= FILE_ATTRIBUTE_HIDDEN;
817
+ else
818
+ attributes &= ~FILE_ATTRIBUTE_HIDDEN;
819
+ end
820
+
821
+ if SetFileAttributes(self.path, attributes) == 0
822
+ raise ArgumentError, get_last_error
823
+ end
824
+ self
825
+ end
826
+
827
+ # Sets the 'indexed' attribute to true or false. Setting this to
828
+ # false means that the file will not be indexed by the content indexing
829
+ # service.
830
+ #
831
+ def indexed=(bool)
832
+ attributes = GetFileAttributes(self.path)
833
+
834
+ if attributes == INVALID_FILE_ATTRIBUTES
835
+ raise ArgumentError, get_last_error
836
+ end
837
+
838
+ if bool
839
+ attributes &= ~FILE_ATTRIBUTE_NOT_CONTENT_INDEXED;
840
+ else
841
+ attributes |= FILE_ATTRIBUTE_NOT_CONTENT_INDEXED;
842
+ end
843
+
844
+ if SetFileAttributes(self.path, attributes) == 0
845
+ raise ArgumentError, get_last_error
846
+ end
847
+
848
+ self
849
+ end
850
+
851
+ alias :content_indexed= :indexed=
852
+
853
+ # Sets the normal attribute. Note that only 'true' is a valid argument,
854
+ # which has the effect of removing most other attributes. Attempting to
855
+ # pass any value except true will raise an ArgumentError.
856
+ #
857
+ def normal=(bool)
858
+ unless bool
859
+ raise ArgumentError, "only 'true' may be passed as an argument"
860
+ end
861
+
862
+ if SetFileAttributes(self.path, FILE_ATTRIBUTE_NORMAL) == 0
863
+ raise ArgumentError, get_last_error
864
+ end
865
+
866
+ self
867
+ end
868
+
869
+ # Sets whether or not a file is online or not. Setting this to false means
870
+ # that the data of the file is not immediately available. This attribute
871
+ # indicates that the file data has been physically moved to offline storage.
872
+ # This attribute is used by Remote Storage, the hierarchical storage
873
+ # management software.
874
+ #
875
+ # Applications should not arbitrarily change this attribute.
876
+ #
877
+ def offline=(bool)
878
+ attributes = GetFileAttributes(self.path)
879
+
880
+ if attributes == INVALID_FILE_ATTRIBUTES
881
+ raise ArgumentError, get_last_error
882
+ end
883
+
884
+ if bool
885
+ attributes |= FILE_ATTRIBUTE_OFFLINE;
886
+ else
887
+ attributes &= ~FILE_ATTRIBUTE_OFFLINE;
888
+ end
889
+
890
+ if SetFileAttributes(self.path, attributes) == 0
891
+ raise ArgumentError, get_last_error
892
+ end
893
+
894
+ self
895
+ end
896
+
897
+ # Sets the readonly attribute. If set to true the the file or directory is
898
+ # readonly. Applications can read the file but cannot write to it or delete
899
+ # it. In the case of a directory, applications cannot delete it.
900
+ #
901
+ def readonly=(bool)
902
+ attributes = GetFileAttributes(self.path)
903
+
904
+ if attributes == INVALID_FILE_ATTRIBUTES
905
+ raise ArgumentError, get_last_error
906
+ end
907
+
908
+ if bool
909
+ attributes |= FILE_ATTRIBUTE_READONLY;
910
+ else
911
+ attributes &= ~FILE_ATTRIBUTE_READONLY;
912
+ end
913
+
914
+ if SetFileAttributes(self.path, attributes) == 0
915
+ raise ArgumentError, get_last_error
916
+ end
917
+
918
+ self
919
+ end
920
+
921
+ # Sets the file to a sparse (usually image) file. Note that you cannot
922
+ # remove the sparse property from a file.
923
+ #
924
+ def sparse=(bool)
925
+ unless bool
926
+ warn 'Cannot remove sparse property from a file - operation ignored'
927
+ return
928
+ end
929
+
930
+ bytes = [0].pack('L')
931
+
932
+ handle = CreateFile(
933
+ self.path,
934
+ FILE_READ_DATA | FILE_WRITE_DATA,
935
+ FILE_SHARE_READ | FILE_SHARE_WRITE,
936
+ 0,
937
+ OPEN_EXISTING,
938
+ FSCTL_SET_SPARSE(),
939
+ 0
940
+ )
941
+
942
+ if handle == INVALID_HANDLE_VALUE
943
+ raise ArgumentError, get_last_error
944
+ end
945
+
946
+ val = DeviceIoControl(
947
+ handle,
948
+ FSCTL_SET_SPARSE(),
949
+ 0,
950
+ 0,
951
+ 0,
952
+ 0,
953
+ bytes,
954
+ 0
955
+ )
956
+
957
+ if val == 0
958
+ raise ArgumentError, get_last_error
959
+ end
960
+
961
+ self
962
+ end
963
+
964
+ # Set whether or not the file is a system file. A system file is a file
965
+ # that is part of the operating system or is used exclusively by it.
966
+ #
967
+ def system=(bool)
968
+ attributes = GetFileAttributes(self.path)
969
+
970
+ if attributes == INVALID_FILE_ATTRIBUTES
971
+ raise ArgumentError, get_last_error
972
+ end
973
+
974
+ if bool
975
+ attributes |= FILE_ATTRIBUTE_SYSTEM;
976
+ else
977
+ attributes &= ~FILE_ATTRIBUTE_SYSTEM;
978
+ end
979
+
980
+ if SetFileAttributes(self.path, attributes) == 0
981
+ raise ArgumentError, get_last_error
982
+ end
983
+
984
+ self
985
+ end
986
+
987
+ # Sets whether or not the file is being used for temporary storage.
988
+ #
989
+ # File systems avoid writing data back to mass storage if sufficient cache
990
+ # memory is available, because often the application deletes the temporary
991
+ # file shortly after the handle is closed. In that case, the system can
992
+ # entirely avoid writing the data. Otherwise, the data will be written
993
+ # after the handle is closed.
994
+ #
995
+ def temporary=(bool)
996
+ attributes = GetFileAttributes(self.path)
997
+
998
+ if attributes == INVALID_FILE_ATTRIBUTES
999
+ raise ArgumentError, get_last_error
1000
+ end
1001
+
1002
+ if bool
1003
+ attributes |= FILE_ATTRIBUTE_TEMPORARY;
1004
+ else
1005
+ attributes &= ~FILE_ATTRIBUTE_TEMPORARY;
1006
+ end
1007
+
1008
+ if SetFileAttributes(self.path, attributes) == 0
1009
+ raise ArgumentError, get_last_error
1010
+ end
1011
+
1012
+ self
1013
+ end
1014
+
1015
+ # Singleton aliases, mostly for backwards compatibility
1016
+ class << self
1017
+ alias :read_only? :readonly?
1018
+ alias :content_indexed? :indexed?
1019
+ alias :set_attr :set_attributes
1020
+ alias :unset_attr :remove_attributes
1021
+ end
1022
+
1023
+ private
1024
+
1025
+ # This is based on the CTL_CODE macro in WinIoCtl.h
1026
+ def CTL_CODE(device, function, method, access)
1027
+ ((device) << 16) | ((access) << 14) | ((function) << 2) | (method)
1028
+ end
1029
+
1030
+ def FSCTL_SET_COMPRESSION
1031
+ CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 16, 0, FILE_READ_DATA | FILE_WRITE_DATA)
1032
+ end
1033
+
1034
+ def FSCTL_SET_SPARSE
1035
+ CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 49, 0, FILE_SPECIAL_ACCESS)
1036
+ end
1037
+ end