win32-file 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
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