milesj / uploader

[Deprecated] A CakePHP plugin for file uploading and validating.
MIT License
193 stars 73 forks source link

Uploads with dynamic paths not deleted #127

Closed SergeR closed 11 years ago

SergeR commented 11 years ago

I'm not sure if that is an issue. I try to generate dynamic paths, it works, but files aren't deleted after delete() method call.

The attached_files table:

public $fields = array(
'id' => array('type' => 'integer', 'null' => false, 'default' => null, 'key' => 'primary'),
'model' => array('type' => 'string', 'null' => false, 'default' => null, 'length' => 100, 'key' => 'index', 'collate' => 'utf8_general_ci', 'charset' => 'utf8'),
'foreign_id' => array('type' => 'integer', 'null' => false, 'default' => null),
'image' => array('type' => 'string', 'null' => false, 'default' => null, 'collate' => 'utf8_general_ci', 'charset' => 'utf8'),
'indexes' => array(
'PRIMARY' => array('column' => 'id', 'unique' => 1),
'model' => array('column' => array('model', 'foreign_id'), 'unique' => 0)
),
'tableParameters' => array('charset' => 'utf8', 'collate' => 'utf8_general_ci', 'engine' => 'InnoDB')
);

Example of model, that has many images.:

App::uses('AppModel', 'Model');

/**
 * Exposition Model
 *
 * @property TicketQuota $TicketQuota
 * @property AttachedFile $Image Attached images
 */
class Exposition extends AppModel {

    public $actsAs = array('Tree', 'Containable');

    /**
     * Validation rules
     *
     * @var array
     */
    public $validate = array(
        'name' => array(
            'notempty' => array(
                'rule' => array('notempty'),
                'required' => true,
                'on' => 'create', // Limit validation to 'create' or 'update' operations
            ),
            'notemptyUpdate' => array(
                'rule' => array('notempty'),
                'required' => false,
                'on' => 'update', // Limit validation to 'create' or 'update' operations
            ),
        ),
    );

    /**
     * hasMany associations
     *
     * @var array
     */
    public $hasMany = array(
        'Image' => array(
            'className' => 'AttachedFile',
            'foreignKey' => 'foreign_id',
            'conditions' => array('Image.model'=>'Exposition'),
            'dependent' => true
        )
    );

 // If record has a children,  don't delete it, just mark as deleted
    public function delete($id = null, $cascade = true) {

        if (!$id && !$this->id)
            return FALSE;

        if (!$id)
            $id = $this->id;

        $children_cnt = $this->childCount($id);

        if ($children_cnt) {
            $this->id = $id;
            $this->saveField('deleted', 1);
            return TRUE;
        }

        return parent::delete($id, $cascade);
    }

    public function undelete($id = NULL) {
        if (!$id && !$this->id)
            return FALSE;

        if ($id)
            $this->id = $id;

        $this->saveField('deleted', '0');
        return TRUE;
    }

    public function beforeValidate($options = array()) {

        if(isset($this->data['Image']) && !empty($this->data['Image'])) {
            $this->Image->setForeignModel($this->alias);
        }

        return parent::beforeValidate($options);
    }

}

The model AttachedFile with Uploader behavior:

class AttachedFile extends AppModel {

    public $actsAs = array(
        'Uploader.Attachment' => array(
            'image' => array(
                'uploadDir' => UPLOAD_DIR,
                'finalPath' => '/files/'
            )
        )
    );

    /**
     * Name of foreign model to associate with
     *
     * @var string
     */
    private $foreignModel = null;

    /**
     *
     * @param string $model
     */
    public function setForeignModel($model) {
        $this->foreignModel = $model;
    }

    public function beforeValidate($options = array()) {

        $this->data[$this->alias]['model'] = $this->foreignModel;

        return parent::beforeValidate($options);
    }

    // Set path to /files/{$foreign_model}/
    // /files/Publication -- for pulications
    // /files/Post -- for blog posts
    // e t.c.
    public function beforeUpload($options=array()) {

        if($this->foreignModel) {
            $options['uploadDir'] = UPLOAD_DIR . DS . $this->foreignModel . DS;
            $options['finalPath'] = '/files/' . $this->foreignModel .'/';
        }

        return $options;
    }
}

When I call the delete() method of Exposition model to delete the record (that has no children):

$this->Exposition->delete($id);

the rows in the AttachedFile model are deleted, but the files in the /files/Exposition directory aren't

milesj commented 11 years ago

It looks like it fails because AttachedFile::$foreignModel is empty when delete is called. You need to set it to the value of what it was when the file was saved.

SergeR commented 11 years ago

AttachedFile::$foreignModel preperty holds the name of currenly linked model. It's just for set a 'model' field of AttachedFile 'magically' when saving. This value stored in the separate fied ('model'). I think I need to set the 'uploadDir' properly before deleting.

SergeR commented 11 years ago

Yeah. The AttachmentBehavior calls protected methos _deleteFile(), and

$basepath = $attachment['uploadDir'] : $attachment['tempDir'];
//file to delete
$file = new File($basepath .  basename($path));

So the change of uploadDir before deleting may help.

milesj commented 11 years ago

Place a debug($options) in beforeUpload() to see if the uploadDir is correct?

SergeR commented 11 years ago

Initially 'uploadDir' points to document_root/files/.

In the beforeUpload method I change it to the other value (document_root/files/Exposition/). The filename in database stored as /files/Exposition/image.jpg. After uploading file is placed in the appropriate directory. Upload process works fine, without any problems.

When I try to call a delete() method, row in the db table is deleted but file remains in the directory. I can't (or I don't know how to) set uploadDir before delete.

SergeR commented 11 years ago

A test for my model.

Fixture:

<?php
/**
 * AttachedFileFixture
 *
 */
class AttachedFileFixture extends CakeTestFixture {

/**
 * Fields
 *
 * @var array
 */
    public $fields = array(
        'id' => array('type' => 'integer', 'null' => false, 'default' => null, 'key' => 'primary'),
        'model' => array('type' => 'string', 'null' => false, 'default' => null, 'length' => 100, 'key' => 'index', 'collate' => 'utf8_general_ci', 'charset' => 'utf8'),
        'foreign_id' => array('type' => 'integer', 'null' => false, 'default' => null),
        'image' => array('type' => 'string', 'null' => false, 'default' => null, 'collate' => 'utf8_general_ci', 'charset' => 'utf8'),
        'indexes' => array(
            'PRIMARY' => array('column' => 'id', 'unique' => 1),
            'model' => array('column' => array('model', 'foreign_id'), 'unique' => 0)
        ),
        'tableParameters' => array('charset' => 'utf8', 'collate' => 'utf8_general_ci', 'engine' => 'InnoDB')
    );

/**
 * Records
 *
 * @var array
 */
    public $records = array(
        array(
            'id' => 1,
            'model' => 'Exposition',
            'foreign_id' => 1,
            'image' => '/files/Exposition/test'
        ),
    );

}

Test

<?php
App::uses('AttachedFile', 'Model');
App::uses('File', 'Utility');
App::uses('Folder', 'Utility');

define('UPLOAD_DIR', TMP . DS . 'files' . DS);

/**
 * AttachedFile Test Case
 *
 */
class AttachedFileTest extends CakeTestCase {

    public $fixtures = array(
        'app.attached_file',
    );

    public function setUp() {

        parent::setUp();
        $this->AttachedFile = ClassRegistry::init('AttachedFile');
    }

    public function tearDown() {
        unset($this->AttachedFile);

        parent::tearDown();
    }

        public function testDelete()
        {

            //Create directories
            $folder = new Folder(UPLOAD_DIR . 'Exposition', TRUE, 0777);

            // Create a file as in the fixture's image field
            $file = new File(UPLOAD_DIR . 'Exposition' .DS . 'test', TRUE);
            $file->write('Foo and Bar');
            $file->close();

            $delResult = $this->AttachedFile->delete(1);
            $this->assertTrue($delResult, 'Model delete method fail');

            // Is record deleted?
            $findRes = $this->AttachedFile->findById(1);
            $this->assertEmpty($findRes, 'Record not deleted');

            $this->assertFalse($file->exists(), 'File not deleted!');
        }

}
milesj commented 11 years ago

Like I mentioned before, you need to set $foreignModel before you delete.

$this->AttachedFile->foreignModel = 'Exposition';
$delResult = $this->AttachedFile->delete(1);
milesj commented 11 years ago

Bump.

SergeR commented 11 years ago

:) Sorry. You're right, it works.