1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10:
11:
12: namespace Nette\Caching;
13:
14: use Nette;
15:
16:
17:
18: 19: 20: 21: 22:
23: class FileJournal extends Nette\Object implements ICacheJournal
24: {
25:
26: const FILE = 'btfj.dat';
27:
28:
29: const FILE_MAGIC = 0x6274666A;
30:
31:
32: const INDEX_MAGIC = 0x696E6465;
33:
34:
35: const DATA_MAGIC = 0x64617461;
36:
37:
38: const NODE_SIZE = 4096;
39:
40:
41: const BITROT = 12;
42:
43:
44: const HEADER_SIZE = 4096;
45:
46:
47: const INT32_SIZE = 4;
48:
49:
50: const USE_JSON = FALSE;
51:
52: const INFO = 'i',
53: TYPE = 't', 54: IS_LEAF = 'il', 55: PREV_NODE = 'p', 56: END = 'e',
57: MAX = 'm', 58: INDEX_DATA = 'id',
59: LAST_INDEX = 'l';
60:
61: 62: const TAGS = 't',
63: PRIORITY = 'p',
64: ENTRIES = 'e';
65:
66: const DATA = 'd',
67: KEY = 'k', 68: DELETED = 'd'; 69:
70:
71: public static $debug = FALSE;
72:
73:
74: private $file;
75:
76:
77: private $handle;
78:
79:
80: private $lastNode = 2;
81:
82:
83: private $lastModTime = NULL;
84:
85:
86: private $nodeCache = array();
87:
88:
89: private $nodeChanged = array();
90:
91:
92: private $toCommit = array();
93:
94:
95: private $deletedLinks = array();
96:
97:
98: private $dataNodeFreeSpace = array();
99:
100:
101: private static $startNode = array(
102: self::TAGS => 0,
103: self::PRIORITY => 1,
104: self::ENTRIES => 2,
105: self::DATA => 3,
106: );
107:
108:
109:
110: 111: 112: 113:
114: public function __construct($dir)
115: {
116: $this->file = $dir . '/' . self::FILE;
117:
118: 119: if (!file_exists($this->file)) {
120: $init = @fopen($this->file, 'xb'); 121: if (!$init) {
122: clearstatcache();
123: if (!file_exists($this->file)) {
124: throw new \InvalidStateException("Cannot create journal file $this->file.");
125: }
126: } else {
127: $writen = fwrite($init, pack('N2', self::FILE_MAGIC, $this->lastNode));
128: fclose($init);
129: if ($writen !== self::INT32_SIZE * 2) {
130: throw new \InvalidStateException("Cannot write journal header.");
131: }
132: }
133: }
134:
135: $this->handle = fopen($this->file, 'r+b');
136:
137: if (!$this->handle) {
138: throw new \InvalidStateException("Cannot open journal file '$this->file'.");
139: }
140:
141: if (!flock($this->handle, LOCK_SH)) {
142: throw new \InvalidStateException('Cannot acquire shared lock on journal.');
143: }
144:
145: $header = stream_get_contents($this->handle, 2 * self::INT32_SIZE, 0);
146:
147: flock($this->handle, LOCK_UN);
148:
149: list(, $fileMagic, $this->lastNode) = unpack('N2', $header);
150:
151: if ($fileMagic !== self::FILE_MAGIC) {
152: fclose($this->handle);
153: $this->handle = false;
154: throw new \InvalidStateException("Malformed journal file '$this->file'.");
155: }
156: }
157:
158:
159:
160: 161: 162:
163: public function __destruct()
164: {
165: if ($this->handle) {
166: $this->headerCommit();
167: flock($this->handle, LOCK_UN); 168: fclose($this->handle);
169: $this->handle = false;
170: }
171: }
172:
173:
174:
175: 176: 177: 178: 179: 180:
181: public function write($key, array $dependencies)
182: {
183: $this->lock();
184:
185: $priority = !isset($dependencies[Cache::PRIORITY]) ? FALSE : (int) $dependencies[Cache::PRIORITY];
186: $tags = empty($dependencies[Cache::TAGS]) ? FALSE : (array) $dependencies[Cache::TAGS];
187:
188: $exists = FALSE;
189: $keyHash = crc32($key);
190: list($entriesNodeId, $entriesNode) = $this->findIndexNode(self::ENTRIES, $keyHash);
191:
192: if (isset($entriesNode[$keyHash])) {
193: $entries = $this->mergeIndexData($entriesNode[$keyHash]);
194: foreach ($entries as $link => $foo) {
195: $dataNode = $this->getNode($link >> self::BITROT);
196: if ($dataNode[$link][self::KEY] === $key) {
197: if ($dataNode[$link][self::TAGS] == $tags && $dataNode[$link][self::PRIORITY] === $priority) { 198: if ($dataNode[$link][self::DELETED]) {
199: $dataNode[$link][self::DELETED] = FALSE;
200: $this->saveNode($link >> self::BITROT, $dataNode);
201: }
202: $exists = TRUE;
203: } else { 204: $toDelete = array();
205: foreach ($dataNode[$link][self::TAGS] as $tag) {
206: $toDelete[self::TAGS][$tag][$link] = TRUE;
207: }
208: if ($dataNode[$link][self::PRIORITY] !== FALSE) {
209: $toDelete[self::PRIORITY][$dataNode[$link][self::PRIORITY]][$link] = TRUE;
210: }
211: $toDelete[self::ENTRIES][$keyHash][$link] = TRUE;
212: $this->cleanFromIndex($toDelete);
213: $entriesNode = $this->getNode($entriesNodeId); 214: unset($dataNode[$link]);
215: $this->saveNode($link >> self::BITROT, $dataNode);
216: }
217: break;
218: }
219: }
220: }
221:
222: if ($exists === FALSE) {
223: 224: if (self::USE_JSON) {
225: $requiredSize = strlen($key) + 45 + substr_count($key, '/');
226: if ($tags) {
227: foreach ($tags as $tag) {
228: $requiredSize += strlen($tag) + 3 + substr_count($tag, '/');
229: }
230: }
231: $requiredSize += $priority ? strlen($priority) : 5;
232: } else {
233: $requiredSize = strlen($key) + 75;
234: if ($tags) {
235: foreach ($tags as $tag) {
236: $requiredSize += strlen($tag) + 13;
237: }
238: }
239: $requiredSize += $priority ? 10 : 1;
240: }
241:
242: $freeDataNode = $this->findFreeDataNode($requiredSize);
243: $data = $this->getNode($freeDataNode);
244:
245: if ($data === FALSE) {
246: $data = array(
247: self::INFO => array(
248: self::LAST_INDEX => ($freeDataNode << self::BITROT),
249: self::TYPE => self::DATA,
250: )
251: );
252: }
253:
254: $dataNodeKey = ++$data[self::INFO][self::LAST_INDEX];
255: $data[$dataNodeKey] = array(
256: self::KEY => $key,
257: self::TAGS => $tags ? $tags : array(),
258: self::PRIORITY => $priority,
259: self::DELETED => FALSE,
260: );
261:
262: $this->saveNode($freeDataNode, $data);
263:
264: 265: $entriesNode[$keyHash][$dataNodeKey] = 1;
266: $this->saveNode($entriesNodeId, $entriesNode);
267:
268: 269: if ($tags) {
270: foreach ($tags as $tag) {
271: list($nodeId, $node) = $this->findIndexNode(self::TAGS, $tag);
272: $node[$tag][$dataNodeKey] = 1;
273: $this->saveNode($nodeId, $node);
274: }
275: }
276:
277: 278: if ($priority) {
279: list($nodeId, $node) = $this->findIndexNode(self::PRIORITY, $priority);
280: $node[$priority][$dataNodeKey] = 1;
281: $this->saveNode($nodeId, $node);
282: }
283: }
284:
285: $this->commit();
286: $this->unlock();
287: }
288:
289:
290:
291: 292: 293: 294: 295:
296: public function clean(array $conditions)
297: {
298: $this->lock();
299:
300: if (!empty($conditions[Cache::ALL])) {
301: $this->nodeCache = $this->nodeChanged = $this->dataNodeFreeSpace = array();
302: $this->deleteAll();
303: $this->unlock();
304: return;
305: }
306:
307: $toDelete = array(
308: self::TAGS => array(),
309: self::PRIORITY => array(),
310: self::ENTRIES => array()
311: );
312:
313: $entries = array();
314:
315: if (!empty($conditions[Cache::TAGS])) {
316: $entries = $this->cleanTags((array) $conditions[Cache::TAGS], $toDelete);
317: }
318:
319: if (isset($conditions[Cache::PRIORITY])) {
320: $this->arrayAppend($entries, $this->cleanPriority((int) $conditions[Cache::PRIORITY], $toDelete));
321: }
322:
323: $this->deletedLinks = array();
324: $this->cleanFromIndex($toDelete);
325:
326: $this->commit();
327: $this->unlock();
328:
329: return $entries;
330: }
331:
332:
333:
334: 335: 336: 337: 338:
339: private function cleanTags(array $tags, array &$toDelete)
340: {
341: $entries = array();
342:
343: foreach ($tags as $tag) {
344: list($nodeId, $node) = $this->findIndexNode(self::TAGS, $tag);
345:
346: if (isset($node[$tag])) {
347: $ent = $this->cleanLinks($this->mergeIndexData($node[$tag]), $toDelete);
348: $this->arrayAppend($entries, $ent);
349: }
350: }
351:
352: return $entries;
353: }
354:
355:
356:
357: 358: 359: 360: 361: 362:
363: private function cleanPriority($priority, array &$toDelete)
364: {
365: list($nodeId, $node) = $this->findIndexNode(self::PRIORITY, $priority);
366:
367: ksort($node);
368:
369: $allData = array();
370:
371: foreach ($node as $prior => $data) {
372: if ($prior === self::INFO) {
373: continue;
374: } elseif ($prior > $priority) {
375: break;
376: }
377:
378: $this->arrayAppendKeys($allData, $this->mergeIndexData($data));
379: }
380:
381: $nodeInfo = $node[self::INFO];
382: while ($nodeInfo[self::PREV_NODE] !== -1) {
383: $nodeId = $nodeInfo[self::PREV_NODE];
384: $node = $this->getNode($nodeId);
385:
386: if ($node === FALSE) {
387: if (self::$debug) throw new \InvalidStateException("Cannot load node number $nodeId.");
388: break;
389: }
390:
391: $nodeInfo = $node[self::INFO];
392: unset($node[self::INFO]);
393:
394: foreach ($node as $prior => $data) {
395: $this->arrayAppendKeys($allData, $this->mergeIndexData($data));
396: }
397: }
398:
399: return $this->cleanLinks($allData, $toDelete);
400: }
401:
402:
403:
404: 405: 406: 407: 408: 409:
410: private function cleanLinks(array $data, array &$toDelete)
411: {
412: $return = array();
413:
414: $data = array_keys($data);
415: sort($data);
416: $max = count($data);
417: $data[] = 0;
418: $i = 0;
419:
420: while ($i < $max) {
421: $searchLink = $data[$i];
422:
423: if (isset($this->deletedLinks[$searchLink])) {
424: ++$i;
425: continue;
426: }
427:
428: $nodeId = $searchLink >> self::BITROT;
429: $node = $this->getNode($nodeId);
430:
431: if ($node === FALSE) {
432: if (self::$debug) throw new \InvalidStateException('Cannot load node number ' . ($nodeId) . '.');
433: ++$i;
434: continue;
435: }
436:
437: do {
438: $link = $data[$i];
439:
440: if (!isset($node[$link])){
441: if (self::$debug) throw new \InvalidStateException("Link with ID $searchLink is not in node ". ($nodeId) . '.');
442: continue;
443: } elseif (isset($this->deletedLinks[$link])) {
444: continue;
445: }
446:
447: $nodeLink = &$node[$link];
448: if (!$nodeLink[self::DELETED]) {
449: $nodeLink[self::DELETED] = TRUE;
450: $return[] = $nodeLink[self::KEY];
451: } else {
452: foreach ($nodeLink[self::TAGS] as $tag) {
453: $toDelete[self::TAGS][$tag][$link] = TRUE;
454: }
455: if ($nodeLink[self::PRIORITY] !== FALSE) {
456: $toDelete[self::PRIORITY][$nodeLink[self::PRIORITY]][$link] = TRUE;
457: }
458: $toDelete[self::ENTRIES][crc32($nodeLink[self::KEY])][$link] = TRUE;
459: unset($node[$link]);
460: $this->deletedLinks[$link] = TRUE;
461: }
462: } while (($data[++$i] >> self::BITROT) === $nodeId);
463:
464: $this->saveNode($nodeId, $node);
465: }
466:
467: return $return;
468: }
469:
470:
471:
472: 473: 474: 475:
476: private function cleanFromIndex(array $toDeleteFromIndex)
477: {
478: foreach ($toDeleteFromIndex as $type => $toDelete) {
479: ksort($toDelete);
480:
481: while (!empty($toDelete)) {
482: reset($toDelete);
483: $searchKey = key($toDelete);
484: list($masterNodeId, $masterNode) = $this->findIndexNode($type, $searchKey);
485:
486: if (!isset($masterNode[$searchKey])) {
487: if (self::$debug) throw new \InvalidStateException('Bad index.');
488: unset($toDelete[$searchKey]);
489: continue;
490: }
491:
492: foreach ($toDelete as $key => $links) {
493: if (isset($masterNode[$key])) {
494: foreach ($links as $link => $foo) {
495: if (isset($masterNode[$key][$link])) {
496: unset($masterNode[$key][$link], $links[$link]);
497: }
498: }
499:
500: if (!empty($links) && isset($masterNode[$key][self::INDEX_DATA])) {
501: $this->cleanIndexData($masterNode[$key][self::INDEX_DATA], $links, $masterNode[$key]);
502: }
503:
504: if (empty($masterNode[$key])) {
505: unset($masterNode[$key]);
506: }
507: unset($toDelete[$key]);
508: } else {
509: break;
510: }
511: }
512: $this->saveNode($masterNodeId, $masterNode);
513: }
514: }
515: }
516:
517:
518:
519: 520: 521: 522: 523:
524: private function mergeIndexData(array $data)
525: {
526: while (isset($data[self::INDEX_DATA])) {
527: $id = $data[self::INDEX_DATA];
528: unset($data[self::INDEX_DATA]);
529: $childNode = $this->getNode($id);
530:
531: if ($childNode === FALSE) {
532: if (self::$debug) throw new \InvalidStateException("Cannot load node number $id.");
533: break;
534: }
535:
536: $this->arrayAppendKeys($data, $childNode[self::INDEX_DATA]);
537: }
538:
539: return $data;
540: }
541:
542:
543:
544: 545: 546: 547: 548: 549: 550:
551: private function cleanIndexData($nextNodeId, array $links, &$masterNodeLink)
552: {
553: $prev = -1;
554:
555: while ($nextNodeId && !empty($links)) {
556: $nodeId = $nextNodeId;
557: $node = $this->getNode($nodeId);
558:
559: if ($node === FALSE) {
560: if (self::$debug) throw new \InvalidStateException("Cannot load node number $nodeId.");
561: break;
562: }
563:
564: foreach ($links as $link => $foo) {
565: if (isset($node[self::INDEX_DATA][$link])) {
566: unset($node[self::INDEX_DATA][$link], $links[$link]);
567: }
568: }
569:
570: if (isset($node[self::INDEX_DATA][self::INDEX_DATA])) {
571: $nextNodeId = $node[self::INDEX_DATA][self::INDEX_DATA];
572: } else {
573: $nextNodeId = FALSE;
574: }
575:
576: if (empty($node[self::INDEX_DATA]) || (count($node[self::INDEX_DATA]) === 1 && $nextNodeId)) {
577: if ($prev === -1) {
578: if ($nextNodeId === FALSE) {
579: unset($masterNodeLink[self::INDEX_DATA]);
580: } else {
581: $masterNodeLink[self::INDEX_DATA] = $nextNodeId;
582: }
583: } else {
584: $prevNode = $this->getNode($prev);
585: if ($prevNode === FALSE) {
586: if (self::$debug) throw new \InvalidStateException("Cannot load node number $prev.");
587: } else {
588: if ($nextNodeId === FALSE) {
589: unset($prevNode[self::INDEX_DATA][self::INDEX_DATA]);
590: if (empty($prevNode[self::INDEX_DATA])) {
591: unset($prevNode[self::INDEX_DATA]);
592: }
593: } else {
594: $prevNode[self::INDEX_DATA][self::INDEX_DATA] = $nextNodeId;
595: }
596:
597: $this->saveNode($prev, $prevNode);
598: }
599: }
600: unset($node[self::INDEX_DATA]);
601: } else {
602: $prev = $nodeId;
603: }
604:
605: $this->saveNode($nodeId, $node);
606: }
607: }
608:
609:
610:
611: 612: 613: 614: 615:
616: private function getNode($id)
617: {
618: 619: if (isset($this->nodeCache[$id])) {
620: return $this->nodeCache[$id];
621: }
622:
623: $binary = stream_get_contents($this->handle, self::NODE_SIZE, self::HEADER_SIZE + self::NODE_SIZE * $id);
624:
625: if (empty($binary)) {
626: 627: return FALSE;
628: }
629:
630: list(, $magic, $lenght) = unpack('N2', $binary);
631: if ($magic !== self::INDEX_MAGIC && $magic !== self::DATA_MAGIC) {
632: if (!empty($magic)) {
633: if (self::$debug) throw new \InvalidStateException("Node $id has malformed header.");
634: $this->deleteNode($id);
635: }
636: return FALSE;
637: }
638:
639: $data = substr($binary, 2 * self::INT32_SIZE, $lenght - 2 * self::INT32_SIZE);
640:
641: if (self::USE_JSON) {
642: $node = @json_decode($data, TRUE); 643: } else {
644: $node = @unserialize($data); 645: }
646:
647: if ($node === FALSE) {
648: $this->deleteNode($id);
649: if (self::$debug) throw new \InvalidStateException("Cannot deserialize node number $id.");
650: return FALSE;
651: }
652:
653: 654: return $this->nodeCache[$id] = $node;
655: }
656:
657:
658:
659: 660: 661: 662: 663: 664:
665: private function saveNode($id, array $node)
666: {
667: if (count($node) === 1) { 668: $nodeInfo = $node[self::INFO];
669: if ($nodeInfo[self::TYPE] !== self::DATA) {
670:
671: if ($nodeInfo[self::END] !== -1) {
672: $this->nodeCache[$id] = $node;
673: $this->nodeChanged[$id] = TRUE;
674: return;
675: }
676:
677: if ($nodeInfo[self::MAX] === -1) {
678: $max = PHP_INT_MAX;
679: } else {
680: $max = $nodeInfo[self::MAX];
681: }
682:
683: list(, , $parentId) = $this->findIndexNode($nodeInfo[self::TYPE], $max, $id);
684: if ($parentId !== -1 && $parentId !== $id) {
685: $parentNode = $this->getNode($parentId);
686: if ($parentNode === FALSE) {
687: if (self::$debug) throw new \InvalidStateException("Cannot load node number $parentId.");
688: } else {
689: if ($parentNode[self::INFO][self::END] === $id) {
690: if (count($parentNode) === 1) {
691: $parentNode[self::INFO][self::END] = -1;
692: } else {
693: end($parentNode);
694: $lastKey = key($parentNode);
695: $parentNode[self::INFO][self::END] = $parentNode[$lastKey];
696: unset($parentNode[$lastKey]);
697: }
698: } else {
699: unset($parentNode[$nodeInfo[self::MAX]]);
700: }
701:
702: $this->saveNode($parentId, $parentNode);
703: }
704: }
705:
706: if ($nodeInfo[self::TYPE] === self::PRIORITY) { 707: if ($nodeInfo[self::MAX] === -1) {
708: if ($nodeInfo[self::PREV_NODE] !== -1) {
709: $prevNode = $this->getNode($nodeInfo[self::PREV_NODE]);
710: if ($prevNode === FALSE) {
711: if (self::$debug) throw new \InvalidStateException('Cannot load node number ' . $nodeInfo[self::PREV_NODE] . '.');
712: } else {
713: $prevNode[self::INFO][self::MAX] = -1;
714: $this->saveNode($nodeInfo[self::PREV_NODE], $prevNode);
715: }
716: }
717: } else {
718: list($nextId, $nextNode) = $this->findIndexNode($nodeInfo[self::TYPE], $nodeInfo[self::MAX] + 1, NULL, $id);
719: if ($nextId !== -1 && $nextId !== $id) {
720: $nextNode[self::INFO][self::PREV_NODE] = $nodeInfo[self::PREV_NODE];
721: $this->saveNode($nextId, $nextNode);
722: }
723: }
724: }
725: }
726: $this->nodeCache[$id] = FALSE;
727: } else {
728: $this->nodeCache[$id] = $node;
729: }
730: $this->nodeChanged[$id] = TRUE;
731: }
732:
733:
734:
735: 736: 737: 738:
739: private function commit()
740: {
741: do {
742: foreach ($this->nodeChanged as $id => $foo) {
743: if ($this->prepareNode($id, $this->nodeCache[$id])) {
744: unset($this->nodeChanged[$id]);
745: }
746: }
747: } while (!empty($this->nodeChanged));
748:
749: foreach ($this->toCommit as $node => $str) {
750: $this->commitNode($node, $str);
751: }
752: $this->toCommit = array();
753: }
754:
755:
756:
757: 758: 759: 760: 761: 762:
763: private function prepareNode($id, $node)
764: {
765: if ($node === FALSE) {
766: if ($id < $this->lastNode) {
767: $this->lastNode = $id;
768: }
769: unset($this->nodeCache[$id]);
770: unset($this->dataNodeFreeSpace[$id]);
771: $this->deleteNode($id);
772: return TRUE;
773: }
774:
775: if (self::USE_JSON) {
776: $data = json_encode($node);
777: } else {
778: $data = serialize($node);
779: }
780:
781: $dataSize = strlen($data) + 2 * self::INT32_SIZE;
782:
783: $isData = $node[self::INFO][self::TYPE] === self::DATA;
784: if ($dataSize > self::NODE_SIZE) {
785: if ($isData) {
786: throw new \InvalidStateException('Saving node is bigger than maximum node size.');
787: } else {
788: $this->bisectNode($id, $node);
789: return FALSE;
790: }
791: }
792:
793: $this->toCommit[$id] = pack('N2', $isData ? self::DATA_MAGIC : self::INDEX_MAGIC, $dataSize) . $data;
794:
795: if ($this->lastNode < $id) {
796: $this->lastNode = $id;
797: }
798: if ($isData) {
799: $this->dataNodeFreeSpace[$id] = self::NODE_SIZE - $dataSize;
800: }
801:
802: return TRUE;
803: }
804:
805:
806:
807: 808: 809: 810: 811: 812:
813: private function commitNode($id, $str)
814: {
815: fseek($this->handle, self::HEADER_SIZE + self::NODE_SIZE * $id);
816: $writen = fwrite($this->handle, $str);
817: if ($writen === FALSE) {
818: throw new \InvalidStateException("Cannot write node number $id to journal.");
819: }
820: }
821:
822:
823:
824: 825: 826: 827: 828: 829:
830: private function findIndexNode($type, $search, $childId = NULL, $prevId = NULL)
831: {
832: $nodeId = self::$startNode[$type];
833:
834: $parentId = -1;
835: while (TRUE) {
836: $node = $this->getNode($nodeId);
837:
838: if ($node === FALSE) {
839: return array(
840: $nodeId,
841: array(
842: self::INFO => array(
843: self::TYPE => $type,
844: self::IS_LEAF => TRUE,
845: self::PREV_NODE => -1,
846: self::END => -1,
847: self::MAX => -1,
848: )
849: ),
850: $parentId,
851: ); 852: }
853:
854: if ($node[self::INFO][self::IS_LEAF] || $nodeId === $childId || $node[self::INFO][self::PREV_NODE] === $prevId) {
855: return array($nodeId, $node, $parentId);
856: }
857:
858: $parentId = $nodeId;
859:
860: if (isset($node[$search])) {
861: $nodeId = $node[$search];
862: } else {
863: foreach ($node as $key => $childNode) {
864: if ($key > $search and $key !== self::INFO) {
865: $nodeId = $childNode;
866: continue 2;
867: }
868: }
869:
870: $nodeId = $node[self::INFO][self::END];
871: }
872: }
873: }
874:
875:
876:
877: 878: 879: 880: 881:
882: private function findFreeNode($count = 1)
883: {
884: $id = $this->lastNode;
885: $nodesId = array();
886:
887: do {
888: if (isset($this->nodeCache[$id])) {
889: ++$id;
890: continue;
891: }
892:
893: $offset = self::HEADER_SIZE + self::NODE_SIZE * $id;
894:
895: $binary = stream_get_contents($this->handle, self::INT32_SIZE, $offset);
896:
897: if (empty($binary)) {
898: $nodesId[] = $id;
899: } else {
900: list(, $magic) = unpack('N', $binary);
901: if ($magic !== self::INDEX_MAGIC && $magic !== self::DATA_MAGIC) {
902: $nodesId[] = $id;
903: }
904: }
905:
906: ++$id;
907: } while (count($nodesId) !== $count);
908:
909: if ($count === 1) {
910: return $nodesId[0];
911: } else {
912: return $nodesId;
913: }
914: }
915:
916:
917:
918: 919: 920: 921: 922:
923: private function findFreeDataNode($size)
924: {
925: foreach ($this->dataNodeFreeSpace as $id => $freeSpace) {
926: if ($freeSpace > $size) {
927: return $id;
928: }
929: }
930:
931: $id = self::$startNode[self::DATA];
932: while (TRUE) {
933: if (isset($this->dataNodeFreeSpace[$id]) || isset($this->nodeCache[$id])) {
934: ++$id;
935: continue;
936: }
937:
938: $offset = self::HEADER_SIZE + self::NODE_SIZE * $id;
939: $binary = stream_get_contents($this->handle, 2 * self::INT32_SIZE, $offset);
940:
941: if (empty($binary)) {
942: $this->dataNodeFreeSpace[$id] = self::NODE_SIZE;
943: return $id;
944: }
945:
946: list(, $magic, $nodeSize) = unpack('N2', $binary);
947: if (empty($magic)) {
948: $this->dataNodeFreeSpace[$id] = self::NODE_SIZE;
949: return $id;
950: } elseif ($magic === self::DATA_MAGIC) {
951: $freeSpace = self::NODE_SIZE - $nodeSize;
952: $this->dataNodeFreeSpace[$id] = $freeSpace;
953:
954: if ($freeSpace > $size) {
955: return $id;
956: }
957: }
958:
959: ++$id;
960: }
961: }
962:
963:
964:
965: 966: 967: 968: 969: 970:
971: private function bisectNode($id, array $node)
972: {
973: $nodeInfo = $node[self::INFO];
974: unset($node[self::INFO]);
975:
976: if (count($node) === 1) {
977: $key = key($node);
978:
979: $dataId = $this->findFreeDataNode(self::NODE_SIZE);
980: $this->saveNode($dataId, array(
981: self::INDEX_DATA => $node[$key],
982: self::INFO => array(
983: self::TYPE => self::DATA,
984: self::LAST_INDEX => ($dataId << self::BITROT),
985: )));
986:
987: unset($node[$key]);
988: $node[$key][self::INDEX_DATA] = $dataId;
989: $node[self::INFO] = $nodeInfo;
990:
991: $this->saveNode($id, $node);
992: return;
993: }
994:
995: ksort($node);
996: $halfCount = ceil(count($node) / 2);
997:
998: list($first, $second) = array_chunk($node, $halfCount, TRUE);
999:
1000: end($first);
1001: $halfKey = key($first);
1002:
1003: if ($id <= 2) { 1004: list($firstId, $secondId) = $this->findFreeNode(2);
1005:
1006: $first[self::INFO] = array(
1007: self::TYPE => $nodeInfo[self::TYPE],
1008: self::IS_LEAF => $nodeInfo[self::IS_LEAF],
1009: self::PREV_NODE => -1,
1010: self::END => -1,
1011: self::MAX => $halfKey,
1012: );
1013: $this->saveNode($firstId, $first);
1014:
1015: $second[self::INFO] = array(
1016: self::TYPE => $nodeInfo[self::TYPE],
1017: self::IS_LEAF => $nodeInfo[self::IS_LEAF],
1018: self::PREV_NODE => $firstId,
1019: self::END => $nodeInfo[self::END],
1020: self::MAX => -1,
1021: );
1022: $this->saveNode($secondId, $second);
1023:
1024: $parentNode = array(
1025: self::INFO => array(
1026: self::TYPE => $nodeInfo[self::TYPE],
1027: self::IS_LEAF => FALSE,
1028: self::PREV_NODE => -1,
1029: self::END => $secondId,
1030: self::MAX => -1,
1031: ),
1032: $halfKey => $firstId,
1033: );
1034: $this->saveNode($id, $parentNode);
1035: } else {
1036: $firstId = $this->findFreeNode();
1037:
1038: $first[self::INFO] = array(
1039: self::TYPE => $nodeInfo[self::TYPE],
1040: self::IS_LEAF => $nodeInfo[self::IS_LEAF],
1041: self::PREV_NODE => $nodeInfo[self::PREV_NODE],
1042: self::END => -1,
1043: self::MAX => $halfKey,
1044: );
1045: $this->saveNode($firstId, $first);
1046:
1047: $second[self::INFO] = array(
1048: self::TYPE => $nodeInfo[self::TYPE],
1049: self::IS_LEAF => $nodeInfo[self::IS_LEAF],
1050: self::PREV_NODE => $firstId,
1051: self::END => $nodeInfo[self::END],
1052: self::MAX => $nodeInfo[self::MAX],
1053: );
1054: $this->saveNode($id, $second);
1055:
1056: list(,, $parent) = $this->findIndexNode($nodeInfo[self::TYPE], $halfKey);
1057: $parentNode = $this->getNode($parent);
1058: if ($parentNode === FALSE) {
1059: if (self::$debug) throw new \InvalidStateException("Cannot load node number $parent.");
1060: } else {
1061: $parentNode[$halfKey] = $firstId;
1062: ksort($parentNode); 1063: $this->saveNode($parent, $parentNode);
1064: }
1065: }
1066: }
1067:
1068:
1069:
1070: 1071: 1072: 1073:
1074: private function headerCommit()
1075: {
1076: fseek($this->handle, self::INT32_SIZE);
1077: @fwrite($this->handle, pack('N', $this->lastNode)); 1078: }
1079:
1080:
1081:
1082: 1083: 1084: 1085: 1086:
1087: private function deleteNode($id)
1088: {
1089: fseek($this->handle, 0, SEEK_END);
1090: $end = ftell($this->handle);
1091:
1092: if ($end <= (self::HEADER_SIZE + self::NODE_SIZE * ($id + 1))) {
1093: $packedNull = pack('N', 0);
1094:
1095: do {
1096: $binary = stream_get_contents($this->handle, self::INT32_SIZE, (self::HEADER_SIZE + self::NODE_SIZE * --$id));
1097: } while (empty($binary) || $binary === $packedNull);
1098:
1099: if (!ftruncate($this->handle, self::HEADER_SIZE + self::NODE_SIZE * ($id + 1))) {
1100: throw new \InvalidStateException('Cannot truncate journal file.');
1101: }
1102: } else {
1103: fseek($this->handle, self::HEADER_SIZE + self::NODE_SIZE * $id);
1104: $writen = fwrite($this->handle, pack('N', 0));
1105: if ($writen !== self::INT32_SIZE) {
1106: throw new \InvalidStateException("Cannot delete node number $id from journal.");
1107: }
1108: }
1109: }
1110:
1111:
1112:
1113: 1114: 1115: 1116:
1117: private function deleteAll()
1118: {
1119: if (!ftruncate($this->handle, self::HEADER_SIZE)) {
1120: throw new \InvalidStateException('Cannot truncate journal file.');
1121: }
1122: }
1123:
1124:
1125:
1126: 1127: 1128: 1129:
1130: private function lock()
1131: {
1132: if (!$this->handle) {
1133: throw new \InvalidStateException('File journal file is not opened');
1134: }
1135:
1136: if (!flock($this->handle, LOCK_EX)) {
1137: throw new \InvalidStateException('Cannot acquire exclusive lock on journal.');
1138: }
1139:
1140: if ($this->lastModTime !== NULL) {
1141: clearstatcache();
1142: if ($this->lastModTime < @filemtime($this->file)) { 1143: $this->nodeCache = $this->dataNodeFreeSpace = array();
1144: }
1145: }
1146: }
1147:
1148:
1149:
1150: 1151: 1152: 1153:
1154: private function unlock()
1155: {
1156: if ($this->handle) {
1157: fflush($this->handle);
1158: flock($this->handle, LOCK_UN);
1159: clearstatcache();
1160: $this->lastModTime = @filemtime($this->file); 1161: }
1162: }
1163:
1164:
1165:
1166: 1167: 1168: 1169: 1170: 1171: 1172:
1173: private function arrayAppend(array &$array, array $append)
1174: {
1175: foreach ($append as $value) {
1176: $array[] = $value;
1177: }
1178: }
1179:
1180:
1181:
1182: 1183: 1184: 1185: 1186: 1187: 1188:
1189: private function arrayAppendKeys(array &$array, array $append)
1190: {
1191: foreach ($append as $key => $value) {
1192: $array[$key] = $value;
1193: }
1194: }
1195:
1196: }
1197: